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

# 13 - Local Testing

> Install your plugin locally and run it end-to-end with the formae CLI: init a test project with @local resolution, apply a forma, verify, and clean up.

With CI running, let's test the plugin locally using the formae CLI. This lets you verify your plugin works end-to-end before deploying it.

## Install the plugin

Build and install the plugin to your local formae plugins directory:

```bash theme={"languages":{"custom":["/languages/pkl.json"]}}
make install
```

This installs the plugin to `~/.pel/formae/plugins/sftp/v0.1.0/`.

## Start SFTP server

Start the test SFTP server:

```bash theme={"languages":{"custom":["/languages/pkl.json"]}}
docker run -d --name sftp-test -p 2222:22 atmoz/sftp testuser:testpass:::upload
```

## Initialize a test project

Use `formae project init` to scaffold a new project that uses your plugin. Since your plugin's PKL schema hasn't been published to the formae package registry yet, use the `@local` suffix to resolve it from your local installation:

```bash theme={"languages":{"custom":["/languages/pkl.json"]}}
mkdir sftp-test-project && cd sftp-test-project
formae project init --include sftp@local
```

The `@local` suffix tells formae to resolve the package from your local plugin directory (`~/.pel/formae/plugins/`) instead of the package registry at `hub.platform.engineering`.

This creates a `PklProject` file with your plugin configured as a local dependency:

```kotlin theme={"languages":{"custom":["/languages/pkl.json"]}}
amends "pkl:Project"

dependencies {
  ["formae"] {
    uri = "package://hub.platform.engineering/plugins/pkl/schema/pkl/formae/formae@0.80.0"
  }
  // Local plugin resolved from ~/.pel/formae/plugins/SFTP/v0.1.0/schema/pkl/
  ["sftp"] = import("/home/you/.pel/formae/plugins/SFTP/v0.1.0/schema/pkl/PklProject")
}
```

The formae core dependency always comes from the package registry, while your plugin (marked with `@local`) is resolved from the local installation directory.

You can mix published and local packages. For example, if your plugin depends on AWS resources for testing:

```bash theme={"languages":{"custom":["/languages/pkl.json"]}}
formae project init --include aws --include sftp@local
```

This resolves `aws` from the registry and `sftp` from your local installation.

## Create a test forma

Edit the generated `main.pkl` to test your plugin:

```kotlin theme={"languages":{"custom":["/languages/pkl.json"]}}
amends "@formae/forma.pkl"
import "@formae/formae.pkl"
import "@sftp/sftp.pkl"

forma {
  new formae.Stack {
    label = "local-test"
    description = "Local testing stack"
  }

  new formae.Target {
    label = "sftp-local"
    config = new sftp.Config {
      url = "sftp://localhost:2222"
    }
  }

  new sftp.File {
    label = "hello"
    path = "/upload/hello.txt"
    content = "Hello from formae!"
    permissions = "0644"
  }
}
```

## Start the agent

In a separate terminal, start the formae agent:

```bash theme={"languages":{"custom":["/languages/pkl.json"]}}
SFTP_USERNAME=testuser SFTP_PASSWORD=testpass formae agent start
```

The agent discovers your plugin and starts it. You should see the following message in the logs:

```text theme={"languages":{"custom":["/languages/pkl.json"]}}
Plugin registered: namespace=SFTP resources=1
```

## Apply the forma

With the agent running, apply your forma:

```bash theme={"languages":{"custom":["/languages/pkl.json"]}}
formae apply --mode reconcile main.pkl
```

```text theme={"languages":{"custom":["/languages/pkl.json"]}}
Command will
├── create target sftp-local
└── create resource hello
    ├── of type SFTP::Files::File
    └── in stack local-test
This operation will create 1 target(s) and create 1 resource(s).

Do you want to continue? (Y): Y

┌──────────────┬─────────┬─────────┬────────┬─────────┬──────┐
│      ID      │ Command │ Status  │ Change │ Success │ Fail │
├──────────────┼─────────┼─────────┼────────┼─────────┼──────┤
│ 38jmr9w3J... │ apply   │ Success │ 2      │ 2       │ 0    │
└──────────────┴─────────┴─────────┴────────┴─────────┴──────┘
```

## Verify the resource

Check that the file was created:

```bash theme={"languages":{"custom":["/languages/pkl.json"]}}
formae inventory resources --query="stack:local-test"
```

```text theme={"languages":{"custom":["/languages/pkl.json"]}}
┌───────────────────┬────────────┬───────────────────┬───────┐
│     NativeID      │   Stack    │       Type        │ Label │
├───────────────────┼────────────┼───────────────────┼───────┤
│ /upload/hello.txt │ local-test │ SFTP::Files::File │ hello │
└───────────────────┴────────────┴───────────────────┴───────┘
```

Or connect to the SFTP server directly:

```bash theme={"languages":{"custom":["/languages/pkl.json"]}}
sshpass -p testpass sftp -P 2222 testuser@localhost
sftp> ls /upload/
hello.txt
sftp> cat /upload/hello.txt
Hello from formae!
```

## Clean up

Destroy the resources by passing the same forma file:

```bash theme={"languages":{"custom":["/languages/pkl.json"]}}
formae destroy --watch main.pkl
```

Stop the test server:

```bash theme={"languages":{"custom":["/languages/pkl.json"]}}
docker rm -f sftp-test
```

***

Next: [14 - Observability](/plugin-development/tutorial/14-observability) - Add logging and metrics to your plugin
