@testsigma/testsigma-cli
v0.0.2
Published
The CLI: validate, pull, push and run, as thin shims over the library seams.
Maintainers
Keywords
Readme
@testsigma/cli
validate, pull, push and run as thin shims over the library seams.
Behaviour is tested at S1–S3; the tests here are smoke tests, which is what the
ticket asks for.
testsigma validate [--root <dir>] [--json]
testsigma push <file> [--root <dir>] [--json] [--dry-run]
[--overwrite-remote] [--discard-healing]
testsigma run [--root <dir>] [--test <name>]
testsigma pull <file | kind name-or-id> [--root <dir>] [--json]
[--write | --adopt-id]pull and push read a live tenant, so they need credentials in the
environment — env vars only, no flags (prototype §1):
export TESTSIGMA_API_KEY=<a key from Settings → API keys>
export TESTSIGMA_HOST=https://app.testsigma.comThe CLI's own piece of work
Everything except one file is a shim. src/workspace.ts is the exception, and
it exists because nothing below the CLI reads a directory:
- Discovery is by convention —
tests/testsigma/<project>/<application>/<version>/and its kind roots, so any repo's tests are found the same way (SPEC user story 9). Environments and the variable pool hang off the project, because that is the only scope the server gives them, and one project's environments are read once and shared by every version under it. - The server coordinates live in the markers and nowhere else, one per
level.
project.sigmacarries the project id,application.sigmathe application id,version.sigmathe version id,folder.sigmaa server folder's canonical name and id. The four names are reserved: a working copy taking one is refused (TSF2064) rather than read as a marker. They are entity files like any other, so their coordinates are read by the same compiler rather than by a second parser. An application missing one is reported asTSF2051against its own path, and the walk carries on to the others: an author fixing a workspace wants everything wrong with it, not the first thing wrong with it. - The walk is total. Every
.sigmafile under the workspace root is read and handed on with its real path, however oddly placed, becauseplacementOfis what decides whether a path is in the layout. Walking only the paths the layout allows left every misplaced file invisible — which is exactly the file worth reporting.
Cross-file checks run through validateWorkspace. A screen filed under
tests/ is TSF2050, because the kind root is what tells push which endpoint a
file goes to; so is a test under tests/ named login.screen.sigma, which is
the misfiling an author actually makes. Two screens claiming one element name is
TSF2052 — the name is unique per application version on the server, so a push
would collide, and a locator lookup would otherwise silently pick one. Where a
file sits in the folder mirror is TSF2058 (at a foldered kind's root, so in no
server folder), TSF2059 (project-scoped kind inside an application) and
TSF2060 (a folder directory whose marker is missing, so it names no folder —
the directory name is a derived handle and never stands in for one).
validate is the contract
--json emits diagnosticsToJson over every file in the workspace — the same
shape a single file's diagnostics take, so the authoring loop parses one format
whether it asked about one file or all of them (SPEC user story 2). A workspace
that cannot be opened at all answers in that shape too, as TSF2051, so the
loop's parser is not conditional on which failure it hit.
pull is the first command that reaches the server
testsigma pull <file> fetches the entity the file's identity binding names,
renders the canonical pull, and prints a unified text diff of the local bytes
against it. It touches nothing unless asked, which is what makes it the safe
first thing to run against a live tenant; --write and --adopt-id are the two
ways of asking, and the read-only report names them both.
The one-spelling law is what makes that diff worth reading: every authoring
state has exactly one surface form, so two files differ textually exactly when
they differ semantically. There are no structured hunks under --json, only the
one diff string and a changed boolean.
- A changed diff still exits 0. A nonzero exit would poison a
set -escript that only wanted to look;changedis what carries the answer (prototype decision 3). - A file that does not compile still diffs, with a notice — the diff is pure text, and a broken file is exactly when a reader wants to see the server's.
- A stale binding is "entity gone" (
TSS1401), reported with both exits named and exit 2 — the same shape and exit as push's stale-id refusal (decision 5). - Prose goes to stdout when the command did its job and to stderr when it did
not, so
testsigma pull … > review.diffholds a diff and nothing else.--jsonalways goes to stdout, because it is the answer.
pull --write and --adopt-id are the only two writes
src/write.ts is the disk half of them, and it is the CLI's second piece of
real work for the same reason src/workspace.ts is the first: nothing below the
CLI knows there is a filesystem.
- The canonical file is the stamped file.
--writestamps the content baseline (ADR-0008), so the text it writes carries its ownhash. The read-only diff is taken against that same text — against the unstamped render, every file pull had ever written would differ on its own hash line for ever. - One derivation, planned before it is performed.
destinationForruns in every mode, so the read-only report can say which rename--writewould follow and which numeric suffix a taken handle would take; onlyperformWritetouches disk. A report naming one path and a write using another is the drift the one-handle rule exists to prevent. - A folder directory is recognised by its marker's id, never by its name — the directory name is a derived handle that does not round-trip, so matching on it would mirror one server folder twice the moment it was renamed. Only the levels no marker claims are created.
- What is already there is read off the disk, not off the workspace walk: a file that failed to compile never reaches the walk and is very much on the filesystem, and the numeric suffix is what stops it being written over.
--adopt-idis a text insertion, not a re-render. Everything but the binding stays byte-identical, so a file whose spacing an author has not got round to fixing keeps it; a model rendered back out would canonicalise it. The insertion point comes from the format's own lexer (tokensOf), because a name may hold a bracket and a second tokeniser would be a second answer to what a bracket is. It stamps no baseline — adopt claims identity, not content reconciliation, so adopt-then-push honestly gets the missing-baseline notice.- A failed write reports what landed, exit 3, with the untried rows marked
not-attempted— the same account push's ledger keeps (ADR-0007), because the caller's next move is decided by what is on disk, not by what was meant.
src/report.ts renders the envelope @testsigma/sync builds. Two rules from
the prototype are binding and tested: a TSS code sits in its own right-hand
column, and every exit is indented under its finding, so a refusal is never
printed without a way out. One deviation is deliberate — §7 prints its exits
flush left while §5c indents them; both cannot be the rule, and the indent is
what keeps a report with two findings readable.
Two commands still stop at a boundary
There is still no write client for the server and no execution for bt. Rather than pretend, each does its pure half and says where it stops — and exits non-zero, because it did not do the thing it is named for:
| Command | Does | Stops at |
| --- | --- | --- |
| run | builds the bt payload, refusing everything the payload cannot represent | executing it |
push no longer stops short: it runs the preflight, sends the writes in the
order the spec fixes, and writes each id back into the file as it lands.
--dry-run is the preflight and the plan alone.
Exit codes are distinct, because that is what a pipeline gates on — and they are
a function of the report's outcome rather than a second table (prototype §2):
0 success, 1 invalid, 2 refused, 3 a write phase died partway, 4 auth,
69 (EX_UNAVAILABLE) unreachable.
push refuses outright if the file it was given does not validate — a push of
something nobody has checked. Not the whole workspace: push is per-entity, and
refusing to push one file because another one is broken would make a workspace a
unit it is not.
