Skip to main content
← Back to list
01Issue
FeatureShippedUAT
Assigneesstack72

Relationships

#1072 UAT Phase 1c: vault family verb gap-fill

Opened by stack72 · 7/10/2026· Shipped 7/10/2026

Summary

Phase 1c fills coverage gaps in the vault command family. The vault family already has deep coverage for create, get, search, read-secret, and type search. This phase adds the missing verbs: describe, inspect, annotate, delete (secret deletion), migrate, put (stdin and alias), list-keys (with no-arg error pin), and edit. It also consolidates vault/list_test.ts into vault/search_test.ts.

All new files land in the existing model-vault-config-report CI shard.

Dependency

Phase 8a (issue #1059) is landed. No other dependencies.

Existing State (do NOT duplicate)

These vault test files already exist:

  • tests/cli/vault/create_test.ts
  • tests/cli/vault/get_test.ts
  • tests/cli/vault/list_test.ts (to be consolidated into search_test.ts)
  • tests/cli/vault/read_secret_test.ts
  • tests/cli/vault/search_test.ts
  • tests/cli/vault/type/search_test.ts

Adversarial tests with vault coverage (do NOT duplicate):

  • tests/cli/adversarial/security_test.ts — includes vault migrate same-type/unknown-type rejections

Available helpers:

  • createLocalVault(runner, dir, name?) — creates a local_encryption vault
  • createUserVault(runner, dir, name?) — creates a user-level vault
  • putSecret(runner, dir, vault, key, value) — stores a secret
  • putSecretDirect(runner, dir, vault, key, value) — stores via stdin
  • writeKindExtension(runner, dir, "vault", opts?) — installs a vault extension type
  • captureRepoSnapshot(dir) — filesystem snapshot for dry-run assertions

Mock vault fixture: tests/cli/fixtures/extensions/vaults/test/mock-vault.ts — a filesystem-backed vault provider that stores secrets in .swamp/vault-data/<name>.json. Use this as the migrate target (--to-type test/mock-vault).

Verified Contracts

Storage layout (built-in local_encryption)

  • Config: vaults/<type>/<id>.yaml
  • Secrets: .swamp/secrets/local_encryption/<vault>/<key>.enc
  • Annotations: .swamp/secrets/local_encryption/<vault>/.annotations/<key>.enc
  • Key file: .swamp/secrets/local_encryption/<vault>/.key

describe vs get

  • Identical JSON except get adds storagePath
  • BOTH print JSON even in log mode
  • get rejects a stray 3rd arg with a read-secret hint

inspect

  • JSON {vaultName, secretKey, vaultType, sizeBytes, sizeChars, valueType, supportsAnnotations, hasAnnotation, annotation, supportsRefreshHooks, hasRefreshHook, refreshHook}
  • No value field exists in the type — the secret value is never returned
  • Missing vault → notFound with available list
  • Missing key → "Secret '<k>' does not exist in vault '<v>'. Store a secret first with: swamp vault put <v> <k>"

annotate

  • Flags: --url --notes --label K=V (repeatable) --remove-label (repeatable) --clear
  • Merge semantics: existing annotation merged, labels merged, only specified fields updated
  • --clear exclusive with the others (exact UserError)
  • No fields at all → UserError
  • Log: "Annotated <key> in vault <vault> (fields: …)"
  • JSON: {vaultName, secretKey, vaultType, fieldsUpdated, cleared, timestamp, annotation}

delete (secret deletion)

  • Prompt: "Delete secret '<key>' from vault '<vaultName>'?"
  • Decline → {"cancelled":true} / "Operation cancelled."
  • --force skips prompt AND missing-key → no-op with noOp:true (exit 0)
  • WITHOUT --force, missing key → "Secret '<key>' not found in vault '<v>'" exit 1
  • Removes secret + annotation + refresh hook
  • JSON: {vaultName, secretKey, vaultType, timestamp, noOp?}

migrate

  • Target via --to-type + --config '<json>' (invalid JSON → UserError)
  • --to-type omitted + --json"Interactive vault migration is not available in JSON mode. Provide --to-type explicitly."
  • Steps: copy every secret → save new config (same id/NAME, new type) → delete old config file
  • --dry-run returns before mutating: JSON {dryRun:true, vaultName, currentType, currentTypeName, targetType, targetTypeName, secretCount}; log ends "Dry run — no changes made."
  • Execution JSON: {vaultName, previousType, newType, newTypeName, secretsMigrated, timestamp}
  • Same-type/unknown-type rejections already in adversarial — leave there

put

  • Value priority: positional → KEY=VALUE in 2nd arg → stdin (when piped) → interactive hidden prompt (log+TTY)
  • Piped overwrite without --force"Secret '<k>' already exists… Use --force (-f) to overwrite when piping from stdin." (errors instead of prompting)
  • Alias: write-secret
  • Refresh-flag validations: --refresh-ttl requires --refresh-from etc.

list-keys

  • The no-arg form ERRORS despite the help text: "Missing required argument: vault_name…" — pin this and file the doc/behavior mismatch as an upstream bug
  • JSON: {vaultName, vaultType, secretKeys, count}; values never fetched
  • Missing vault → notFound with create-hint or available list

edit

  • Editor-only (no stdin mode, unlike model/workflow edit)
  • No-arg + --json"Vault name or ID is required in non-interactive mode"

New Test Files

vault/describe_test.ts (NEW)

  • JSON shape validation (no storagePath field)
  • Parity assertion: describe output ≡ get output minus storagePath
  • Nonexistent vault → exit 1
  • Exit codes: 0/1

vault/inspect_test.ts (NEW)

  • Metadata shape validated through schema
  • Secret value absent from stdout AND stderr (leak scan with readAllFilesRecursively is overkill here — just assertNotMatch on stdout+stderr for the stored value)
  • sizeBytes and sizeChars are correct for a known value
  • After vault annotate → inspect shows the annotation reflected
  • Missing key → exact hint message "Store a secret first with: swamp vault put <v> <k>"
  • Missing vault → notFound with available list
  • Exit codes: 0/1

vault/annotate_test.ts (NEW)

  • Set url + notes + labels → vault inspect shows them reflected in annotation
  • Second annotate with only --notes → merges (earlier url and labels survive)
  • --remove-label <key> removes only that label, others survive
  • --clear removes all annotation fields
  • --clear combined with --url or --notes → rejected with UserError
  • No fields at all → rejected with UserError
  • Bad label format (missing =) → rejected
  • JSON shape: {vaultName, secretKey, vaultType, fieldsUpdated, cleared, timestamp, annotation}
  • Exit codes: 0/1

vault/delete_test.ts (NEW)

  • --force deletes the secret: read-secret fails after, annotation also gone
  • --force on missing key → noOp:true, exit 0
  • Without --force, missing key → "Secret '<key>' not found in vault '<v>'", exit 1
  • Prompt decline (piped stdin, EOF = "no") → {"cancelled":true} / "Operation cancelled.", secret still readable after
  • JSON shape: {vaultName, secretKey, vaultType, timestamp, noOp?}
  • Exit codes: 0/1

vault/migrate_test.ts (NEW)

  • Happy path: local_encryptiontest/mock-vault (use writeKindExtension to install the mock vault type first): secrets readable after migration via new type, old config YAML gone, vault name preserved
  • --dry-run mutates nothing: use captureRepoSnapshot before and after, assert identical; JSON {dryRun:true, ...} shape
  • --to-type omitted + --json → rejected with exact message
  • Bad --config JSON → rejected with UserError
  • Exit codes: 0/1
  • NOTE: same-type and unknown-type rejections are already covered in adversarial — do NOT duplicate

vault/put_test.ts (NEW)

  • Positional value: vault put <vault> <key> <value> → readable via read-secret
  • KEY=VALUE form in 2nd arg: vault put <vault> KEY=VALUE → stored correctly
  • Stdin value: pipe a value via stdin → stored correctly; trailing newline stripped; inner newlines preserved
  • Piped overwrite without --force → exact error "Secret '<k>' already exists… Use --force (-f) to overwrite when piping from stdin."
  • --force overwrites existing secret
  • write-secret alias parity: same command with write-secret instead of vault put produces identical result
  • Exit codes: 0/1

vault/list_keys_test.ts (EXTEND existing or NEW — check current state)

  • Keys listed without values (values never appear in output — assert absence)
  • JSON shape: {vaultName, vaultType, secretKeys, count}
  • Empty vault → count:0, secretKeys:[]
  • No-arg errors: "Missing required argument: vault_name…", exit 1 — pin this and file upstream bug for the doc/behavior mismatch
  • Nonexistent vault → notFound with hint
  • Exit codes: 0/1

vault/edit_test.ts (NEW)

  • No-arg + --json"Vault name or ID is required in non-interactive mode", exit 1
  • Nonexistent vault → exit 1
  • (Editor-only — no stdin test since vault edit doesn't support stdin mode)
  • Exit codes: 0/1

Consolidation

Fold vault/list_test.ts into vault/search_test.ts — the hidden list alias should produce identical output to search. Add an alias parity assertion to search_test.ts, then delete list_test.ts.

Schemas

Add these new Zod schemas to src/cli/helpers/schemas.ts:

  • VaultDescribeSchema
  • VaultInspectSchema
  • VaultAnnotateSchema
  • VaultDeleteSchema
  • VaultMigrateSchema / VaultMigrateDryRunSchema
  • VaultPutSchema (if put has JSON output — verify in ground truth)
  • VaultListKeysSchema

Use .passthrough() on schemas (matching existing codebase convention).

Ground Rules

  • Read CLAUDE.md in the repo root first — its rules apply everywhere
  • Tests are more accurate than the code under test. Assert the documented contract. If swamp disagrees, the test stays red and you file the discrepancy upstream with swamp issue bug and reference it in a test comment. Never weaken a test to match current behavior.
  • Use the github-pr skill for commits and PR creation
  • Task 0 — ground truth: Before writing any test, build tip-of-main swamp and run each in-scope command by hand in a scratch repo (happy path, --json, one failure). Read the command definitions in ~/code/swamp-club/swamp/src/cli/commands/. Note any drift from this issue in the PR description.
  • Check open swamp-uat PRs for in-flight work touching your files before starting
  • Use grep -rl "vault describe\|vault inspect\|vault annotate\|vault delete\|vault migrate\|vault put\|vault list-keys\|vault edit" tests/cli/ before writing to avoid duplicating existing coverage

Test Pattern

All tests must follow this pattern — real binary, real commands, real exit codes:

import { assertEquals, assertStringIncludes } from "@std/assert";
import {
  createRunnerFromEnv,
  createLocalVault,
  putSecret,
  runCommand,
  runJsonCommand,
  withInitializedRepoContext,
} from "@swamp-uat/helpers";

const runner = createRunnerFromEnv();

Deno.test("swamp vault inspect shows metadata without exposing the value", () =>
  withInitializedRepoContext(runner, async (repo) => {
    await createLocalVault(runner, repo.dir, "secrets");
    await putSecret(runner, repo.dir, "secrets", "api-key", "sk-12345");
    const result = await runJsonCommand(runner, repo.dir, ["vault", "inspect", "secrets", "api-key"], VaultInspectSchema);
    assertEquals(result.sizeChars, 8);
    // Value must never appear in any output
    assertNotMatch(result._raw.stdout, /sk-12345/);
    assertNotMatch(result._raw.stderr, /sk-12345/);
  }));

Every test MUST:

  • Spawn the real swamp binary (via runCommand, runJsonCommand, or runner.render)
  • Assert an explicit exit code
  • Validate --json output through a Zod schema
  • End with waitFor(() => assertEquals(instance.hasExit()?.exitCode, N)) when using runner.render

How to Run & Verify

# 1. Format, lint, type-check
deno task fmt
deno task lint
deno task check

# 2. Run framework unit tests (includes CI guard test)
deno task test

# 3. Run the vault tests directly
SWAMP_NO_TELEMETRY=1 SWAMP_SOURCE_DIR=~/code/swamp-club/swamp deno test --allow-read --allow-write --allow-env --allow-run --allow-net tests/cli/vault/

# 4. Run the full CLI suite to check for regressions
SWAMP_NO_TELEMETRY=1 SWAMP_SOURCE_DIR=~/code/swamp-club/swamp deno task uat:cli

No Docker/ministack needed — all vault tests use local_encryption (built-in) and the mock-vault fixture extension (filesystem-backed mock). The mock-vault is the migration target type.

Definition of Done

  • 8 test files created/extended per the table above (describe, inspect, annotate, delete, migrate, put, list-keys, edit)
  • vault/list_test.ts consolidated into vault/search_test.ts (alias parity), list_test.ts deleted
  • All Zod schemas added to src/cli/helpers/schemas.ts
  • Every test spawns the real swamp binary — no mocking, no importing swamp internals
  • Every test asserts an explicit exit code
  • Every --json test validates through a Zod schema
  • No-arg list-keys error pinned AND upstream bug filed with swamp issue bug
  • Upstream discrepancies filed with swamp issue bug and referenced in test comments
  • deno task fmt && deno task lint && deno task check && deno task test all green
  • SWAMP_NO_TELEMETRY=1 SWAMP_SOURCE_DIR=~/code/swamp-club/swamp deno task uat:cli passes
  • PR opened via github-pr skill, branching from main
02Bog Flow
OPENTRIAGEDIN PROGRESSSHIPPED+ 1 MOREASSIGNED+ 2 MOREREVIEW+ 4 MOREPR_MERGED+ 1 MORENOTIFICATION_SKIPPED

Shipped

7/10/2026, 11:35:22 PM

Click a lifecycle step above to view its details.

03Sludge Pulse
stack72 assigned stack727/10/2026, 8:57:16 PM

Sign in to post a ripple.