{"_id":"@akhera-horus/testenv","_rev":"3-8f2c1cbf5dbc582c7b4d8288a6aa541d","name":"@akhera-horus/testenv","dist-tags":{"latest":"0.2.0"},"versions":{"0.1.0":{"name":"@akhera-horus/testenv","version":"0.1.0","keywords":["testenv","schema","manifest","testing"],"license":"MIT","_id":"@akhera-horus/testenv@0.1.0","maintainers":[{"name":"arjunkhera","email":"arjunkherais@gmail.com"}],"dist":{"shasum":"aeda1f0c961a74ede96504ad55963bc29f11a697","tarball":"https://registry.npmjs.org/@akhera-horus/testenv/-/testenv-0.1.0.tgz","fileCount":4,"integrity":"sha512-TJ/31k1NxkCKL8sgrlDXA8E2plWFBiA180RdMqZr2MtCcrHONwqRHTvgvT/EbCG947Hjxu/9qQYLAXAb7J4i/A==","signatures":[{"sig":"MEUCIDSTODOfZADKBpKJQ2Bm0mZU0M4pGstvlH4Sg6hXZrKfAiEAtmhB2bxO321X/J3/+6R4Po3lAY4afZ2gdZ8dNMi4Lkk=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":346953},"main":"./dist/index.js","type":"module","types":"./dist/index.d.ts","engines":{"node":">=18"},"exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js"}},"gitHead":"88feb68b10d553287bc4235d452db8fffdca5061","scripts":{"dev":"tsup src/index.ts --format esm --watch","test":"vitest run","build":"tsup src/index.ts --format esm --dts","typecheck":"tsc --noEmit"},"_npmUser":{"name":"arjunkhera","email":"arjunkherais@gmail.com"},"_npmVersion":"10.9.4","description":"testenv/v1 declarative manifest schema, types, and validator","directories":{},"_nodeVersion":"22.22.0","dependencies":{"zod":"^3.23.0"},"_hasShrinkwrap":false,"devDependencies":{"tsup":"^8.0.0","vitest":"^4.0.18","typescript":"^5.4.0","@types/node":"^20.0.0"},"_npmOperationalInternal":{"tmp":"tmp/testenv_0.1.0_1779012801079_0.055341827259466125","host":"s3://npm-registry-packages-npm-production"}},"0.1.1":{"name":"@akhera-horus/testenv","version":"0.1.1","keywords":["testenv","schema","manifest","testing"],"license":"MIT","_id":"@akhera-horus/testenv@0.1.1","maintainers":[{"name":"arjunkhera","email":"arjunkherais@gmail.com"}],"dist":{"shasum":"5789c1ab93797c9262ed17fbc7a0b9e13463ed00","tarball":"https://registry.npmjs.org/@akhera-horus/testenv/-/testenv-0.1.1.tgz","fileCount":5,"integrity":"sha512-B6rGFKKuwEHpimdo9hOK+sTog4J5pCiXwNBLvPqFHADS5HEN+31svVaAf018SDuk1h91jmz+9luIqAxeUWDCYA==","signatures":[{"sig":"MEYCIQCxRuWTBnG0RMlFvsHM5wu8CpL+uvPWt1zEwqMJXhE36wIhAMPZMQHNx7SXV5wQJSLaq8w+Ht2MqH+oClSQsCj+m7ZJ","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":382843},"main":"./dist/index.js","type":"module","_from":"file:akhera-horus-testenv-0.1.1.tgz","types":"./dist/index.d.ts","engines":{"node":">=18"},"exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js"}},"scripts":{"dev":"tsup src/index.ts --format esm --watch","test":"vitest run","build":"tsup src/index.ts --format esm --dts","typecheck":"tsc --noEmit"},"_npmUser":{"name":"arjunkhera","email":"arjunkherais@gmail.com"},"_resolved":"/private/var/folders/55/ymtrdjxj21v2xdl0gtjz8h340000gn/T/313ef1aafa2deca8e4a242d75b6449ad/akhera-horus-testenv-0.1.1.tgz","_integrity":"sha512-B6rGFKKuwEHpimdo9hOK+sTog4J5pCiXwNBLvPqFHADS5HEN+31svVaAf018SDuk1h91jmz+9luIqAxeUWDCYA==","_npmVersion":"10.9.4","description":"testenv/v1 declarative manifest schema, types, and validator","directories":{},"_nodeVersion":"22.22.0","dependencies":{"zod":"^3.23.0"},"_hasShrinkwrap":false,"devDependencies":{"tsup":"^8.0.0","vitest":"^4.0.18","typescript":"^5.4.0","@types/node":"^20.0.0"},"_npmOperationalInternal":{"tmp":"tmp/testenv_0.1.1_1779074863510_0.5288602213109981","host":"s3://npm-registry-packages-npm-production"}},"0.2.0":{"name":"@akhera-horus/testenv","version":"0.2.0","description":"testenv/v1 declarative manifest schema, types, and validator","type":"module","main":"./dist/index.js","types":"./dist/index.d.ts","exports":{".":{"import":"./dist/index.js","types":"./dist/index.d.ts"}},"scripts":{"build":"tsup src/index.ts --format esm --dts","dev":"tsup src/index.ts --format esm --watch","typecheck":"tsc --noEmit","test":"vitest run"},"dependencies":{"zod":"^3.23.0"},"devDependencies":{"@types/node":"^20.0.0","tsup":"^8.0.0","typescript":"^5.4.0","vitest":"^4.0.18"},"engines":{"node":">=18"},"keywords":["testenv","schema","manifest","testing"],"license":"MIT","_id":"@akhera-horus/testenv@0.2.0","gitHead":"35a0c1245a2e8f5fcf1bd9c7ea6145b1bdd88e83","_nodeVersion":"22.22.0","_npmVersion":"10.9.4","dist":{"integrity":"sha512-VjpRtiGUDC8iLuUPO/HICEQTGWpBLJ/kvWBBJjpPamM98o7CNM4PmwKjwiNu3oKQAKcXCEEBdPkYddyyGhIbaw==","shasum":"8a494ed75af7d7318a8ec7025b6fb5a096cdfa48","tarball":"https://registry.npmjs.org/@akhera-horus/testenv/-/testenv-0.2.0.tgz","fileCount":4,"unpackedSize":379443,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEQCIDClYk1+/+WmiPouSSYE3GQb6/mhEWRSFIchk/kNHm2RAiA9OYy4v2kD+uAby5xfVsvfl83xu6WKW6hhzl77h7Ut5Q=="}]},"_npmUser":{"name":"arjunkhera","email":"arjunkherais@gmail.com"},"directories":{},"maintainers":[{"name":"arjunkhera","email":"arjunkherais@gmail.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/testenv_0.2.0_1779108116828_0.5769197060057718"},"_hasShrinkwrap":false}},"time":{"created":"2026-05-17T10:13:20.989Z","modified":"2026-05-18T12:41:57.213Z","0.1.0":"2026-05-17T10:13:21.220Z","0.1.1":"2026-05-18T03:27:43.649Z","0.2.0":"2026-05-18T12:41:57.089Z"},"license":"MIT","keywords":["testenv","schema","manifest","testing"],"description":"testenv/v1 declarative manifest schema, types, and validator","maintainers":[{"name":"arjunkhera","email":"arjunkherais@gmail.com"}],"readme":"# @horus/testenv — testenv/v1 Schema & Validator\n\nDeclarative manifest format (`testenv/v1`) for isolated, ephemeral test environments.\nRepo-agnostic: no stack-specific assumptions. Per-repo specifics (provisioner commands,\nservice names, port numbers) are data in the manifest, not code in this package.\n\n---\n\n## Overview\n\nA `.testenv/manifest.yaml` file describes how to spin up an isolated copy of a repo's\nstack, run tests against it, and tear it down — with provable isolation invariants at\neach step. The format is consumed by the runner core CLI and the `sdlc-testenv` executor\nsubagent. The run result / event log format is corpus-ready: a future CI regression gate\nreuses it unchanged.\n\n---\n\n## The 6-Phase Spine\n\n```\nsetup → launch → await_ready → connection → test → teardown\n```\n\n| Phase         | Purpose                                                                 |\n|---------------|-------------------------------------------------------------------------|\n| `setup`       | Clone code, build, prepare file layout. Phase-1 isolation checks here. |\n| `launch`      | Bring up the stack (via `provisioner` command or explicit steps). Phase-2 checks. |\n| `await_ready` | Poll until all services are healthy (timeout-bounded).                  |\n| `connection`  | Emit the connection manifest (MCP settings, API base URLs, etc.).       |\n| `test`        | Execute test actions (the do/check/proof units).                        |\n| `teardown`    | Release the stack. Phase-6 detective isolation checks.                  |\n\n---\n\n## Isolation Invariants\n\nThree enforcement points guarantee the test stack never disturbs a co-located live stack:\n\n**Phase 1 — Preventive** (abort before touching anything shared):\n- Disjoint docker project / network / volumes / port space / data directory\n- All declared `requires.secrets` present in the environment\n\n**Phase 2 — Structural** (verified after launch):\n- Distinct docker project name\n- All ports fully projected (no wildcards or overlay-merge)\n- Connection manifest emitted with correctly suffixed MCP server names\n\n**Phase 6 — Detective** (verified post-teardown):\n- Live stack still running and unmodified\n- Live data contains zero test artifacts\n- No secret values in logs or temp files\n\n---\n\n## Test Actions — The Do/Check/Proof Unit\n\nEach test action is a `{invoke, expect, evidence}` triple:\n\n```yaml\n- name: anvil-roundtrip\n  invoke:\n    tool: anvil_create_note\n    args: { title: \"check\" }\n  expect:\n    returns: note_id\n  evidence:\n    capture: response\n  needsStack: true\n```\n\nThree invocation flavors:\n\n1. **Command** — `{ run: \"curl -sf http://localhost:8080/health\" }`\n2. **MCP tool call** — `{ tool: \"anvil_create_note\", args: { title: \"check\" } }`\n3. **HTTP request** — `{ http: \"live-anvil/search?q=check\" }`\n4. **Ordered sequence** — `{ sequence: [{ name, invoke, expect }, ...] }`\n\nEach action declares `needsStack: boolean`. When `false`, the runner skips\nsetup/launch/await_ready/teardown for that action (fast unit/type inner loop).\n\n---\n\n## Manifest Structure\n\n```yaml\napiVersion: testenv/v1\nrepo: my-app\n\nrequires:\n  secrets: [MY_API_KEY, DB_PASSWORD]   # env var NAMES only — values never logged\n  profiles:\n    laptop: { mem: 6g }\n    cloud:  { mem: 12g }\n\nphases:\n  setup:\n    steps:\n      - run: git clone --no-hardlinks {src} {workdir}\n      - run: npm install && npm run build\n    assert:\n      - type: path_exists\n        path: {workdir}/dist\n    isolationChecks:\n      disjointDockerProject: true\n      disjointPortSpace: true\n      allRequiredSecretsPresent: true\n\n  launch:\n    provisioner: \"docker compose -p my-app-test-{slot} up -d\"\n    assert:\n      - type: containers_running\n        count: 3\n      - type: ports_disjoint_from_live\n        value: true\n    isolationChecks:\n      distinctDockerProject: true\n      fullyProjectedPorts: true\n      connectionManifestEmitted: true\n\n  await_ready:\n    poll:\n      - probe: \"GET :{api_port}/health\"\n        expect: http_200\n    timeout: 120\n\n  connection:\n    emit: \"{slot}/settings.json\"\n    assert:\n      - type: file_parses\n        path: \"{slot}/settings.json\"\n        format: json\n\n  test:\n    policy: fail-fast      # or run-all\n    actions:\n      - name: health-check\n        invoke:\n          run: curl -sf http://localhost:{api_port}/health\n        expect:\n          exitCode: 0\n        evidence:\n          capture: stdout\n      - name: isolation-proof\n        invoke:\n          http: \"live-api/search?q=test-artifact\"\n        expect:\n          count: 0\n\n  teardown:\n    provisioner: \"docker compose -p my-app-test-{slot} down -v\"\n    assert:\n      - type: containers_remaining\n        count: 0\n      - type: live_stack_unmodified\n        value: true\n      - type: live_data_test_artifacts_zero\n        value: true\n      - type: no_secret_residue\n        value: true\n    isolationChecks:\n      liveStackUnmodified: true\n      liveDataTestArtifactsZero: true\n      noSecretResidue: true\n```\n\n---\n\n## Template Variables\n\nManifest strings may contain `{var}` placeholders resolved by the runner at execution time:\n\n| Variable    | Description                                                        |\n|-------------|--------------------------------------------------------------------|\n| `{slot}`    | Unique slot identifier for this run (namespaces docker resources). |\n| `{workdir}` | Working directory for the cloned code.                             |\n| `{src}`     | Source repository path.                                            |\n| `{profile}` | Active run profile name (e.g., `laptop`, `cloud`).                |\n\n---\n\n## Supported Assertion Types\n\n| Type                           | Description                                                     |\n|--------------------------------|-----------------------------------------------------------------|\n| `path_exists`                  | File or directory exists at the given path.                     |\n| `cmd_exit_zero`                | Command exits with code 0.                                      |\n| `containers_running`           | Exactly N containers are running in the docker project.         |\n| `containers_remaining`         | Exactly N containers remain after teardown.                     |\n| `ports_disjoint_from_live`     | No port conflicts with the live stack.                          |\n| `docker_project_name`          | Docker project name matches the given pattern.                  |\n| `file_parses`                  | File is valid JSON or YAML.                                     |\n| `env_vars_present`             | All listed env var names are set (non-empty).                   |\n| `live_stack_unmodified`        | Live stack still running and unmodified post-teardown.          |\n| `live_data_test_artifacts_zero`| Live data contains zero test artifacts.                         |\n| `no_secret_residue`            | No secret values in logs or temp files.                         |\n| `mcp_names_suffixed`           | All MCP server names in the connection manifest carry a suffix. |\n| `fully_projected_ports`        | All ports explicitly mapped (no wildcards).                     |\n| `http_ok`                      | HTTP GET returns a 2xx status.                                  |\n\n---\n\n## Run Result / Event Log Format\n\nThe runner emits a machine-readable `RunResult` at the end of each run.\nThe format is corpus-ready: a future CI regression gate consumes it unchanged.\n\nKey fields:\n\n```jsonc\n{\n  \"resultVersion\": \"testenv/v1/result\",\n  \"verdict\": \"pass\",           // pass | fail | skip | error\n  \"runClass\": \"green\",         // red | green | regression | adhoc\n  \"repo\": \"my-app\",\n  \"profile\": \"laptop\",\n  \"commit\": \"abc123\",\n  \"specVersion\": \"def456\",     // commit hash of .testenv at run time\n  \"startedAt\": \"...\",\n  \"endedAt\": \"...\",\n  \"totalDurationMs\": 300000,\n  \"phases\": [...],             // per-phase results\n  \"actions\": [...],            // per-action results (test phase)\n  \"events\": [...]              // full structured event log\n}\n```\n\n`runClass` supports the TDD red→green proof-of-work cycle:\n- `red` — pre-implementation run (expected failures)\n- `green` — post-implementation run (expected passes)\n- `regression` — CI gate run against committed test suite\n- `adhoc` — manual/exploratory run\n\n---\n\n## Usage\n\n```typescript\nimport { validateManifest, assertManifest } from '@horus/testenv';\nimport { parse } from 'yaml';\nimport { readFileSync } from 'node:fs';\n\nconst raw = parse(readFileSync('.testenv/manifest.yaml', 'utf8'));\n\n// Option 1: graceful error handling\nconst result = validateManifest(raw);\nif (result.ok) {\n  console.log('Valid manifest:', result.data);\n} else {\n  for (const err of result.errors) {\n    console.error(`[${err.path}] ${err.message}`);\n  }\n}\n\n// Option 2: fail-fast (throws on invalid)\nconst manifest = assertManifest(raw);\n```\n\n---\n\n## Development\n\n```bash\npnpm install\npnpm build    # tsup ESM build\npnpm test     # vitest unit tests\npnpm typecheck\n```\n","readmeFilename":"README.md"}