> ## Documentation Index
> Fetch the complete documentation index at: https://docs.keldyn.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Address and evidence controls with tests

> Use the Keldyn MCP server to implement framework controls in your codebase, back each one with an automated test, and write evidence that points auditors (and CI) at the real code.

A control that only exists in a spreadsheet fails at audit time. A control backed by an automated test fails in CI — when you can still fix it. This guide shows Keldyn platform users how to drive that loop with an AI agent connected to the [Keldyn MCP server](/keldyn-mcp-server): list the controls for a framework (AppSec is the running example), implement what's missing, add unit/integration tests **only for code-relevant controls**, and update Keldyn evidence so those controls point at the implementing code **and** their tests.

<Note>
  Every MCP tool call runs **as you**, scoped to the team you selected at sign-in. Updating control status or evidence writes to your live workspace — ask the agent to show a plan and sample evidence text before it calls `update-control`.
</Note>

## What “done” looks like

Treat this as the acceptance criteria for the whole program of work (swap `AppSec` for any use-case framework):

| Criterion                   | Meaning in practice                                                                                                                                                                    |
| --------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Implemented**             | Every in-scope control that applies to this system has a real implementation in the repository.                                                                                        |
| **Test-backed (code only)** | Each **code-relevant** control has at least one unit or integration test that would fail if the control stopped holding. Process, infra, and org controls do not need app-level tests. |
| **Evidenced in Keldyn**     | For code-relevant controls, `evidenceText` names implementation path(s) and test path(s), plus a one-line “how”. Other controls get notes, links, or a dated N/A / defer reason.       |
| **Gaps are explicit**       | Anything not implemented (or not testable in this repo) is listed with a **reason** and a **date** — not left as silent “Pending Review”.                                              |

That last row matters as much as the first three. Auditors and future you need a dated disposition for deferred and out-of-scope controls, not an empty status.

## Before you start

<Steps>
  <Step title="Connect the MCP server">
    Follow [Keldyn MCP server](/keldyn-mcp-server) and sign in to the team that owns the use case.
  </Step>

  <Step title="Assign the framework on the use case">
    Open the use case → **Add Framework** → enable **AppSec** (or ISO 27001, CASA, etc.). Controls only appear after the framework is assigned and scoped. See [Frameworks & controls](/frameworks-and-controls).
  </Step>

  <Step title="Open the repository">
    Run the agent from the repo that implements the system so it can read code, add tests, and cite real paths in evidence.
  </Step>

  <Step title="Know your scope">
    Have the **use case ID** ready (`list-use-cases` if you don't). Org-level and use-case tracking are **separate** — pass `useCaseId` on every `update-control` call when you mean the use case.
  </Step>

  <Step title="Know (or load) the architecture">
    Prefer reading the use case’s saved architecture via MCP (`get-use-case`) before inventing hosting assumptions. If deployment, trust boundaries, or runtime topology still aren’t clear, **ask the user** — don’t guess.
  </Step>
</Steps>

## The loop at a glance

| Phase                   | What happens                                                                 | Key MCP tools                                 |
| ----------------------- | ---------------------------------------------------------------------------- | --------------------------------------------- |
| **1. Context**          | Load use case + architecture; ask about deployment if unclear                | `get-use-case`, `list-use-cases`              |
| **2. Inventory**        | List every control for the framework on the use case                         | `list-controls`, `get-control`                |
| **3. Triage**           | Sort into implement / already done / N/A / defer; mark which need code tests | Agent analysis (+ optional `review-codebase`) |
| **4. Implement + test** | For **code-relevant** controls: ship code + unit/integration test            | Your test runner (e.g. Vitest / Jest)         |
| **5. Evidence**         | Write `evidenceText` (code + test paths when applicable); set status         | `update-control`                              |
| **6. Record gaps**      | For anything left open, store reason + date (and usually a task)             | `update-control`, `create-task`               |

## 1. Load architecture (ask when unclear)

Before inventorying controls, ground the agent in how the system actually runs.

```text theme={null}
For use case <USE_CASE_ID>, call get-use-case and read any saved
architecture. Summarize hosting, trust boundaries, and runtimes you
can infer. If deployment or architecture is still unclear, ask me
before assuming (cloud provider, edge/WAF, managed DB, who runs CI,
etc.). Do not invent a topology.
```

<Note>
  Many AppSec dispositions (especially N/A vs defer for edge, network, and platform controls) depend on deployment. Prefer the use case architecture in Keldyn; only interview the user for what that model does not cover.
</Note>

## 2. Inventory the controls

Pull the framework's controls for the use case, then fetch full requirements. Flag which ones are code-relevant (those are the only ones that need unit/integration tests).

```text theme={null}
Using the Keldyn MCP server for use case <USE_CASE_ID>:

1. list-controls with scope "use_case", useCaseIds [<USE_CASE_ID>],
   frameworks ["appsec"] (optionally relevance "code" to focus the
   testable subset; use "all" if you also want process/infra controls
   in the gap list).
2. For each control, get-control so we have description, acceptance
   criteria, and current status / evidenceText.
3. Return a table: rcmId | title | status | already has evidence? |
   code-relevant? | needs unit/integration test? (yes only if
   code-relevant)
```

Under the hood: `list-controls` with `frameworks: ["appsec"]` scopes to AppSec. Use slug `appsec` (or the framework name); other common slugs include `casa`, `iso-27001`, `soc2`, `gdpr`. `relevance: "code"` is useful when you want only the subset that should get unit/integration tests.

<Tip>
  Unit and integration tests are required only for **code-relevant** controls (things enforced in this repository). Process, policy, vendor, and pure infra/edge controls should be evidenced with notes, links, or dated N/A / defer — not forced into the app test suite.
</Tip>

## 3. Triage before you write code

Ask the agent to bucket every control **before** it edits the repo or calls `update-control`:

| Bucket                                | Status to aim for                 | What to produce                                         |
| ------------------------------------- | --------------------------------- | ------------------------------------------------------- |
| **Implement now** (code-relevant)     | → `complete` after code + test    | Implementation + unit/integration test + evidence paths |
| **Already satisfied** (code-relevant) | → `complete` (refresh evidence)   | Evidence pointing at existing code + tests              |
| **Satisfied without app tests**       | → `complete` with notes/links     | Infra/process evidence; no unit test required           |
| **Not applicable**                    | → `ignored`                       | Reason + date in `evidenceText` / `finding`             |
| **Defer**                             | → `in_progress` (or leave `todo`) | Reason + target date; preferably a linked task          |

```text theme={null}
From the AppSec inventory for use case <USE_CASE_ID>, triage every
control into: implement-now | already-satisfied | satisfied-without-app-tests |
not-applicable | defer.

Mark needs unit/integration test? only when the control is
code-relevant (enforced in this repo). For each row give: rcmId,
one-line rationale, and (for defer / N/A) a proposed reason + date.
If architecture/deployment was inferred from get-use-case, state
those assumptions; otherwise ask me. Show the triage table and wait
for my approval before changing code or calling update-control.
```

<Warning>
  For **code-relevant** controls: do not mark `complete` because “the feature exists.” Complete means the acceptance criteria are met **and** a unit/integration test would fail if the control regressed. If you only have implementation, leave status `in_progress` and note the missing test in evidence. Non-code controls can be `complete` without an app test when evidence is documented another way.
</Warning>

## 4. Implement and add tests (code-relevant only)

Work control-by-control (or by theme: auth, input validation, secrets, headers, dependencies). For each **code-relevant** control in **implement-now** / **already-satisfied**:

1. Implement or confirm the control in the platform code.
2. Add or identify a **unit or integration test** that fails if the control stops holding (not a vague smoke test — assert the security property).
3. Run that test locally before updating Keldyn.

Skip creating tests for controls that are not code-relevant (policies, org process, edge/WAF-only, vendor reviews, etc.) — document those under gaps or non-test evidence instead.

```text theme={null}
For AppSec control <RCM_ID> on use case <USE_CASE_ID>:
1. get-control and restate the acceptance criteria in one paragraph.
2. Decide if it is code-relevant. If not, draft N/A or non-test
   evidence and stop — do not invent a unit test.
3. If code-relevant: locate or implement the control in this repo.
4. Add a focused unit or integration test that fails if the control
   regresses. Put it next to the existing suite for that package.
5. Run the new/updated test and show me the result.
6. Draft evidenceText (see format below) but do NOT call update-control
   until I approve.
```

### What a good guardrail test looks like

| Control theme    | Weak test                    | Strong test                                                  |
| ---------------- | ---------------------------- | ------------------------------------------------------------ |
| Authn / session  | “Login page renders”         | Unauthenticated request to a protected route returns 401     |
| Authz / RBAC     | “Admin page loads for admin” | Non-admin token receives 403 on an admin-only API            |
| Input validation | “Form submits”               | Rejected payload (XSS / oversized / wrong type) returns 400  |
| Secrets          | “App starts”                 | Config loader fails closed when a required secret is missing |
| Security headers | Snapshot of HTML             | Response includes expected `Content-Security-Policy` / etc.  |

Prefer unit or fast integration tests that run in the package's default `npm test` / CI job. E2E alone is not the bar for this guide — reserve E2E for flows that truly can’t be asserted lower down.

## 5. Write evidence that points at code **and** tests

Once a code-relevant control’s test is green, update the use-case control with `update-control`:

| Field          | Value                                   |
| -------------- | --------------------------------------- |
| `rcmId`        | Control id (e.g. from AppSec inventory) |
| `useCaseId`    | **Required** for use-case tracking      |
| `status`       | `complete`                              |
| `evidenceText` | Paths + how (see template)              |

### Evidence text template (code-relevant)

Paste this shape into prompts so every code-relevant control gets consistent, auditor-friendly notes:

```text theme={null}
Implementation: <path/to/file.ts> — <one line: what enforces the control>
Test: <path/to/file.test.ts> — <one line: what assertion guards it>
Verified: <YYYY-MM-DD> via <test command or CI job name>
```

Example:

```text theme={null}
Implementation: src/middleware/authenticate.js — rejects requests
without a valid session JWT.
Test: tests/unit/middleware/authenticate.test.js — asserts 401 when
Authorization header is missing or expired.
Verified: 2026-08-11 via the package unit-test command
```

For non-code controls, omit the `Test:` line and describe how the control is satisfied (policy link, hosting config, integration evidence, etc.).

```text theme={null}
For every code-relevant control we finished (code + green
unit/integration test), call update-control with useCaseId
<USE_CASE_ID>, status "complete", and evidenceText using the
Implementation / Test / Verified template. For non-code controls you
are marking complete, use notes/links without inventing a test path.
Show the batch of proposed updates first; apply only after I approve.
```

<Tip>
  Keep evidence paths relative to the repo root and stable (avoid machine-specific absolute paths). When a control spans API + web, list both implementation files and both test files.
</Tip>

## 6. List what you did **not** implement

For every control left open, record a dated disposition. Silent `todo` is the enemy.

### Gap list format (keep in the PR / ticket / evidence)

```markdown theme={null}
| rcmId | Title | Disposition | Reason | Date |
|-------|-------|-------------|--------|------|
| APP-12 | Example | defer | Needs WAF at edge; tracked in OPS-441 | 2026-08-11 |
| APP-19 | Example | not_applicable | Control targets mobile SDK; this product is web-only | 2026-08-11 |
```

In Keldyn:

* **Not applicable** → `update-control` with `status: "ignored"` and the reason + date in `evidenceText` (and optionally `finding`).
* **Defer** → `status: "in_progress"`, same reason + target date in `evidenceText`, plus `create-task` with `rcmId` so the gap is actionable.

```text theme={null}
For every AppSec control on use case <USE_CASE_ID> that we are not
marking complete:
1. Propose status ignored (N/A) or in_progress (defer).
2. Draft evidenceText: "Not implemented: <reason>. Reviewed: <YYYY-MM-DD>."
3. For deferrals, also draft a create-task payload linked with rcmId.
Present the gap table, then apply updates only after I approve.
```

## End-to-end prompts (copy and adapt)

### A. Full AppSec pass (recommended starter)

Improved version of a typical “implement + maintain AppSec with tests” request:

```text theme={null}
Using the Keldyn MCP server, implement and maintain AppSec control
evidence for use case <USE_CASE_ID> so that a code-relevant control
that stops holding fails CI instead of failing an audit.

Acceptance criteria:
- Every AppSec control that applies to this use case is either
  addressed, marked N/A, or deferred with a reason and a date.
- Unit/integration tests are required ONLY for code-relevant controls
  (enforced in this repository). Do not invent app tests for process,
  policy, vendor, or pure infra/edge controls.
- For each code-relevant control you mark complete, evidenceText cites
  implementation path(s) and test path(s) using:
  Implementation: … / Test: … / Verified: …
- Org vs use-case tracking: always pass useCaseId on update-control.

Task:
1. get-use-case and read the saved architecture. Summarize what you
   can infer about deployment and topology. If anything material is
   still unclear (hosting, edge/WAF, managed services, CI), ask me —
   do not guess.
2. list-controls (scope use_case, frameworks ["appsec"]) and
   get-control for each. Note which are code-relevant.
3. Triage into implement-now | already-satisfied |
   satisfied-without-app-tests | not-applicable | defer.
   Show the triage table (include needs unit/integration test?) and
   wait for approval.
4. For approved code-relevant items: implement or confirm code,
   add/identify a unit or integration test, run it, draft evidenceText.
5. After my approval, update-control → complete with that evidence.
6. For gaps / non-code items: update-control with ignored or
   in_progress (or complete with non-test evidence) + reason/date;
   create-task (rcmId) for deferrals.
7. Finish with a summary table: rcmId | status | code-relevant? |
   evidence summary | gap?.

Do not mark a code-relevant control complete without a guarding
unit/integration test. Do not write to Keldyn until I approve each batch.
```

### B. Evidence-only refresh (code already good)

```text theme={null}
For use case <USE_CASE_ID>, frameworks ["appsec"]:
First get-use-case / architecture; ask me about deployment only if
you cannot infer what you need.

Find existing unit/integration tests that already prove each
code-relevant control. Draft evidenceText for controls that are
satisfied but have empty or stale evidence. Non-code controls: draft
notes/links only — no new tests. Show the list; update-control to
complete only after approval. Do not change product code in this pass.
```

### C. Single control deep-dive

```text theme={null}
Focus on AppSec control <RCM_ID> for use case <USE_CASE_ID>.
get-control → decide if it is code-relevant → if yes: map acceptance
criteria to this repo → implement gaps → add a unit/integration test
→ run it → draft evidenceText; if no: draft N/A or non-test evidence
with reason + date. Ask about deployment/architecture only if
get-use-case does not make applicability clear. Wait for approval →
update-control with useCaseId.
```

### D. Gap inventory only (no code changes)

```text theme={null}
List all AppSec controls for use case <USE_CASE_ID>. Do not change code.
Read get-use-case architecture first; ask me about deployment only
where applicability depends on it and you cannot infer it.

Produce a markdown gap table: rcmId | title | current status |
code-relevant? | implemented in repo? | unit/integration test
exists? | proposed disposition | reason | date.
Save nothing to Keldyn unless I ask.
```

## Tips that keep evidence trustworthy

* **Tests only where code owns the control** — if the control isn’t enforced in this repo, don’t add a fake unit test; record N/A, defer, or non-test evidence instead.
* **One control, one guarding assertion** — a shared “security smoke” test that vaguely touches ten controls is weak evidence; prefer a named test per control or a clearly commented assertion block.
* **Ask when architecture is ambiguous** — load the use case architecture first; only ask the user about deployment/topology for what you still can’t infer.
* **CI is part of the story** — put code-relevant tests in the default pipeline. Mention the job or command in the `Verified:` line.
* **Re-evidence when paths move** — refactors that rename files should refresh `evidenceText` in the same PR.
* **Don't over-claim** — edge/WAF/cloud-config controls usually can’t be unit-tested in-app; defer or N/A with a date, or point at infra / integration evidence instead.
* **Related loop** — for a broader “find gaps → tasks → fix” workflow without the test bar, see [From controls to code with MCP](/guides/mcp-controls-to-code).

## Next steps

<CardGroup cols={2}>
  <Card title="Controls-to-code workflow" icon="code" href="/guides/mcp-controls-to-code" arrow={true}>
    Broader loop: analyze the repo, mark controls complete, and open tasks for gaps.
  </Card>

  <Card title="Keldyn MCP server" icon="plug" href="/keldyn-mcp-server" arrow={true}>
    Connect your AI tool and see tools plus built-in prompts like collect-evidence.
  </Card>

  <Card title="Evidence & audits" icon="folder-check" href="/evidence-and-audits" arrow={true}>
    Attach files, links, and integration-collected evidence in the web app.
  </Card>

  <Card title="Frameworks & controls" icon="list-checks" href="/frameworks-and-controls" arrow={true}>
    Assign AppSec (and other frameworks) and track status in the workspace.
  </Card>
</CardGroup>
