@postman/postman-plugin 0.5.0 → 0.6.0-rc.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/README.md +2 -2
  2. package/dist/hosts/factory.js +29 -3
  3. package/hooks/session-start-context.md +1 -1
  4. package/mcp.pi.json +2 -2
  5. package/package.json +1 -1
  6. package/skills/api-discovery/SKILL.md +7 -2
  7. package/skills/api-documentation/SKILL.md +2 -3
  8. package/skills/api-engineer/SKILL.md +13 -15
  9. package/skills/api-mocking/SKILL.md +4 -2
  10. package/skills/api-monitoring/SKILL.md +35 -25
  11. package/skills/api-simulation/SKILL.md +109 -74
  12. package/skills/api-simulation/reference/context_file.md +183 -149
  13. package/skills/api-simulation/reference/sim_yaml_schema.md +1 -1
  14. package/skills/api-testing/SKILL.md +3 -3
  15. package/skills/application-testing/SKILL.md +2 -2
  16. package/skills/ci-integration/SKILL.md +7 -5
  17. package/skills/cli-install/SKILL.md +29 -0
  18. package/skills/collection-schema-v3/SKILL.md +21 -11
  19. package/skills/collection-schema-v3/reference/environment.md +2 -0
  20. package/skills/collection-schema-v3/reference/globals.md +40 -0
  21. package/skills/collection-schema-v3/reference/http.md +149 -11
  22. package/skills/collection-schema-v3/reference/other_protocols.md +232 -86
  23. package/skills/datasets/SKILL.md +6 -5
  24. package/skills/datasets/reference/schema.md +51 -0
  25. package/skills/flows/SKILL.md +0 -1
  26. package/skills/performance-testing/SKILL.md +14 -1
  27. package/skills/performance-testing/references/gatling-import/README.md +141 -0
  28. package/skills/performance-testing/references/gatling-import/feeders.md +31 -0
  29. package/skills/performance-testing/references/gatling-import/injection.md +88 -0
  30. package/skills/performance-testing/references/gatling-import/mapping.md +91 -0
  31. package/skills/performance-testing/references/gatling-import/unsupported.md +31 -0
  32. package/skills/postman-mcp-server/SKILL.md +1 -1
  33. package/skills/variables/SKILL.md +2 -2
  34. package/skills/webhook-testing/SKILL.md +1 -1
  35. package/skills/workspaces/SKILL.md +53 -0
  36. package/skills/bootstrap/SKILL.md +0 -140
  37. package/skills/bootstrap/reference/cli_installation.md +0 -59
@@ -0,0 +1,141 @@
1
+ # Importing a Gatling project into Postman
2
+
3
+ > Reference for the `performance-testing` skill. Convert the simulation, review the gap report, then the result is an ordinary performance run. Verifying it runs one iteration of real requests against the target, so confirm the target first — the same way any action with effects outside this session gets confirmed.
4
+
5
+ ## Overview
6
+ A Gatling simulation is code: a scenario of `http()` requests, checks, feeders,
7
+ pauses and flow control, plus a `setUp` that injects load and asserts on
8
+ results. Postman splits the same thing in two:
9
+ - the **collection** (requests, test scripts, variables, data file), and
10
+ - the **run config** (`postman performance run` flags: VUs, duration, load profile, pass criteria).
11
+
12
+ You do the whole translation by reading the source and applying the files next to
13
+ this one — there is no importer to run. Two disciplines keep you honest, both
14
+ described below: an **inventory** (list every construct first, so nothing is
15
+ dropped silently) and a **self-verify** pass (prove every construct landed
16
+ somewhere or is explained, and that the collection actually runs).
17
+
18
+ A conversion that runs but silently drops a check, or quietly changes the load
19
+ model, is worse than no conversion, because the user will trust the numbers.
20
+ **The gap report is a first-class output, not an apology.**
21
+
22
+ ## Workflow
23
+
24
+ 1. **Discover.** Find the simulation(s): classes that `extends Simulation`,
25
+ Kotlin `: Simulation()`, or JS/TS `simulation((setUp) => …)`. Note the language
26
+ and build tool. If there is more than one simulation, ask which to convert.
27
+ Convert one simulation per output directory.
28
+ 2. **Inventory.** Read every source file the simulation touches and list **every
29
+ construct a conversion must account for** — see *Inventory: what to list*. This
30
+ is your checklist; each item gets a stable id. Pay attention to requests built
31
+ in loops or helper methods and to dynamic names — a shallow scan misses them.
32
+ 3. **Translate** each item using the files next to this one:
33
+ - `mapping.md`: requests, EL, checks, protocol settings, groups, flow control, custom code
34
+ - `feeders.md`: feeders → data file + distribution
35
+ - `injection.md`: injection profiles, throttle, assertions → run flags
36
+ - `unsupported.md`: what becomes a gap, and how to word it
37
+ 4. **Write outputs** to the output directory (formats below).
38
+ 5. **Verify** against the checklist in *Verify: self-review* — including, after you
39
+ confirm the target, one real smoke iteration via the existing
40
+ `postman collection run` command. Fix everything that fails and re-check, up to
41
+ 3 rounds.
42
+ 6. **Report** to the user: counts (mapped / approximated / gap), the verify
43
+ result, and the top 3 things they must do by hand. Point them to MIGRATION.md.
44
+
45
+ ## Inventory: what to list
46
+ List every construct below that appears in the simulation, each with a **stable
47
+ id** (used verbatim in the MIGRATION.md coverage table). Kinds and id prefixes:
48
+
49
+ - `req:<name>` — each `http("name").get/post(...)` request.
50
+ - `check:<req>#<n>` — each check on a request (`status`, `jsonPath`, `jmesPath`, `regex`, `header`, `bodyString`, `substring`, `responseTimeInMillis`, …). A request with **no** `status` check still has Gatling's implicit 2xx-or-304 check — list it as `implicit-status:<req>`.
51
+ - `scenario:<name>`, `group:<name>` — scenarios and groups.
52
+ - `feeder:<file>` and `feed:<n>` — feeder sources and `feed(...)` calls (one logical data source emits both; see `feeders.md`).
53
+ - `timing:<n>` — `pause`, `pace`, `rendezVous`.
54
+ - `flow:<construct>#<n>` — `repeat`, `during`, `forever`, `doIf`, `tryMax`, `exitHereIfFailed`, `doSwitch`, …
55
+ - `inj#<n>` — each injection step (open or closed model).
56
+ - `throttle#<n>`, `max-duration` — throttle / `maxDuration`.
57
+ - `assert#<n>` — each `setUp(...).assertions(...)` entry.
58
+ - `proto:<setting>` — each `http.baseUrl/header/acceptHeader/shareConnections/enableHttp2/...`.
59
+ - `non_http:<proto>:<name>` — `ws`, `sse`, `grpc`, `mqtt`, … steps (and their checks).
60
+ - `code:<n>` — custom-code `exec(session -> …)` lambdas.
61
+ - `secret:<n>` — any literal that looks like a credential (API key, token, `Authorization` value). **Record only that it exists — never copy the literal anywhere.**
62
+
63
+ A request built in a loop or helper, or named dynamically, still gets an id; find it by reading, not pattern-matching. If you are unsure a construct exists, list it and resolve it to a gap rather than omit it.
64
+
65
+ ## Output formats
66
+ ### `collection.json`: Postman collection v2.1
67
+ - `info.schema` = `https://schema.getpostman.com/json/collection/v2.1.0/collection.json`
68
+ - `info.description` is a plain string whose first line is exactly `imported-from: gatling` followed by two lines `source: <relative source path>` and `class: <simulation class/name>`. Example: `"imported-from: gatling\nsource: src/test/java/poc/BasicSimulation.java\nclass: BasicSimulation"`.
69
+ - One collection per simulation. One folder per Gatling `group`; with **no** groups, requests sit at the collection root (a bare request name, no folder prefix — that is valid). Requests keep their Gatling names exactly (they drive per-request metrics), and **request leaf names must be unique** (a collection cannot hold two requests with the same path — see `repeat` in mapping.md).
70
+ - Base URL → collection variable `baseUrl`. Requests use `{{baseUrl}}/path`.
71
+ - Each request's test script ends with the translated checks. Mark translated custom code with `// REVIEW: translated from Gatling custom code (<inventory id>)`.
72
+
73
+ ### `environment.json`: Postman environment
74
+ `{"name": "<simulation> (from Gatling)", "values": [{"key": "...", "value": "...", "enabled": true}]}`.
75
+ Secrets become keys with an **empty value** and `"type": "secret"`. Never copy a
76
+ secret literal from the source (see Critical rules). This file holds **secrets and
77
+ externalised config only** — `baseUrl` and correlation values are collection variables, not
78
+ environment keys. **Always write the file**, even when there is nothing to put in it: a
79
+ simulation with no secrets and no externalised config gets `"values": []`. (An empty
80
+ environment is correct and expected, not a defect.)
81
+
82
+ ### `data/<name>.csv`
83
+ Copy feeder files that the simulation uses. Convert JSON array feeders to CSV if they are flat. If the simulation has **no feeder**, don't create a `data/` directory at all (and omit `--data-file` from run.sh).
84
+
85
+ ### `run.sh`
86
+ ```bash
87
+ #!/usr/bin/env bash
88
+ # Converted from <Simulation>. Replace <collection-uid> after pushing the collection to a workspace.
89
+ postman performance run "<collection-uid>" \
90
+ --environment "<environment-uid>" \
91
+ --vu-count 20 --duration 5 --load-profile ramp-up \
92
+ --data-file data/users.csv \
93
+ --pass-if "less_than(error_rate, 1)"
94
+ ```
95
+ Only flags that exist in `postman performance run --help` (check the user's installed
96
+ version; don't assume a flag from a newer CLI). Duration is in whole minutes, rounded up.
97
+ - `--environment` — include it whenever `environment.json` has any values (the secrets/config requests depend on). Requests using `{{secret}}`/config resolve to nothing without it. Tell the user to import and **populate** `environment.json`, then replace `<environment-uid>` with its UID. Omit the flag only when the environment is empty (`values: []`).
98
+ - `--data-file` only if the simulation has a feeder; **omit the line entirely** when there is none (don't invent a data file).
99
+ - `--pass-if`: **at most one**, chosen by the strict priority in `injection.md` (error-rate, then latency percentile, then mean/rps) — this overrides source order. If **no** source assertion matches any category, **omit `--pass-if` entirely**. Every non-chosen assertion goes to the gap report.
100
+
101
+ ### `MIGRATION.md`: gap report (also your accounting record)
102
+ Sections, in order:
103
+ 1. **Summary**: source, counts (mapped / approximated / gap), verify result. The counts must equal the coverage-table tally; derive them from the table, don't hand-write them.
104
+ 2. **Load model**: the original injection profile, the chosen flags, and the arithmetic (see `injection.md`). Required even when the mapping is exact.
105
+ 3. **Coverage**: a table with one row per inventory id — this is the accounting that replaces a manifest. Columns: `id` · `kind` · `status` (`mapped` / `approximated` / `gap`) · `landed` (where it went, or why it was dropped). **Every inventory id appears here**; a missing row is a silently-dropped construct. Use these idioms in `landed`:
106
+ - request → `collection:<folder>/<request>` (root request → just the name)
107
+ - check / saveAs → `collection:<req> › test` (a `saveAs` value is script-set, so it is a test-script line, not a declared collection variable)
108
+ - protocol setting on every request → name each request it lands on
109
+ - scenario → `the collection itself`; group → `folder <name>`
110
+ - injection → the `run:` flags it set
111
+ - secret → `environment:<key> (placeholder, value not copied)`
112
+ 4. **Approximations**: one bullet per approximated id: `` `<id>` ``, what changed, why, what to check.
113
+ 5. **Gaps**: one bullet per gap id: `` `<id>` ``, what was dropped, why, and the manual workaround if there is one.
114
+ 6. **Secrets**: which environment keys the user must fill in.
115
+ 7. **Next steps**: push the collection, fill secrets, run a short test, compare with a Gatling baseline.
116
+
117
+ Every `approximated` or `gap` id **must appear verbatim** in both the coverage table and its Approximations/Gaps bullet.
118
+
119
+ ## Verify: self-review
120
+ Before claiming done, check each of these against your own output and fix every miss:
121
+
122
+ 1. **Accounting** — every inventory id has a row in the MIGRATION.md coverage table; every `approximated`/`gap` id also has its bullet. Nothing dropped silently.
123
+ 2. **Collection** — valid v2.1, `info.description` stamped `imported-from: gatling`, every request has a method and a URL, and no two requests share the same folder/name path.
124
+ 3. **Variables** — every `{{var}}` used anywhere in the collection is defined somewhere: a collection variable, an environment key, a data-file column, or set by an earlier request's script. An undefined `{{var}}` is a bug.
125
+ 4. **Secrets** — no secret literal appears in any output file (collection, environment, run.sh, MIGRATION.md). Re-read the source's credential-looking values and confirm none were copied.
126
+ 5. **run.sh** — run `postman performance run --help` and confirm every flag you used exists in this CLI version, values are valid (duration a whole number ≥ 1, load profile one of the listed names), and there is **at most one** `--pass-if`.
127
+ 6. **Smoke** — the one executable check. Tell the user it sends **one iteration** of the converted requests to the target host(s), name the host(s), and wait for a yes for any host that is not local. Then run the existing CLI:
128
+
129
+ ```
130
+ postman collection run collection.json -n 1 -e environment.json
131
+ ```
132
+
133
+ (add `-d data/<file>` when there is a feeder). Confirm every request executed with no transport error. A `pm.test` failure that comes from the conversion needs an explanation in MIGRATION.md; one that comes from the target (e.g. a 4xx the simulation also expected) just gets noted. If the user declines the traffic, or there is no CLI login / network, skip only this step and say in your report that no request was executed — the other five checks still hold.
134
+
135
+ ## Critical rules
136
+ 1. **Never copy a secret literal into any output.** That covers the collection, environment, run.sh and MIGRATION.md. Items of kind `secret` become an environment key with an empty value, and MIGRATION.md tells the user to fill it in. Don't put secrets in descriptions or metadata either.
137
+ 2. **Never drop an item silently.** Every inventory id gets a coverage-table row. If you aren't sure, choose `approximated` or `gap` and explain. Don't choose `mapped`.
138
+ 3. **Never change the load model silently.** Open-model (arrival-rate) injection has no exact equivalent. Always show the conversion arithmetic in MIGRATION.md → Load model.
139
+ 4. **Confirm before sending traffic.** The smoke sends one iteration to the target. Get a yes for any non-local host first. Never run `postman performance run` yourself; the user runs `run.sh`.
140
+ 5. **Keep request names identical** to the Gatling `http("…")` names, so results can be compared with old Gatling reports.
141
+ 6. **Don't claim done until the self-verify checklist passes.** If you can't get it to pass in 3 rounds, stop and report what is still failing.
@@ -0,0 +1,31 @@
1
+ # Feeders → data file + distribution
2
+
3
+ ## Sources (`feeder:*`)
4
+ | Gatling | Postman | Status |
5
+ |---|---|---|
6
+ | `csv("f.csv")`, `tsv`, `ssv`, `separatedValues` | copy to `data/f.csv` (convert TSV/SSV to CSV) → `--data-file data/f.csv` | mapped |
7
+ | `jsonFile("f.json")` (a flat array of objects) | `data/f.csv`, or keep JSON (`--data-file` accepts JSON) | mapped |
8
+ | `jsonUrl(url)` | download once (with the user's OK) or leave as a gap | gap unless downloaded |
9
+ | `jdbcFeeder`, `redisFeeder`, `sitemap` | export to CSV by hand | gap. Give the export query in MIGRATION.md |
10
+ | `listFeeder` / `arrayFeeder` (inline data) | write the rows to `data/<name>.csv` | mapped |
11
+
12
+ The file must sit in `$OUT/data/` so that `run.sh` and the smoke run can find it. Set `manifest.run.data_file` to the file the smoke run should use.
13
+
14
+ ## Strategy
15
+ The Gatling default (no strategy) is **`queue`**. Each record is used once, and the run **crashes** when the records run out.
16
+ | Gatling | `postman performance run --data-file` behaviour | Status |
17
+ |---|---|---|
18
+ | `.circular()` | with `--data-file`, local rows are distributed per the CLI's documented default (confirm in the installed `--help`; it has been random in recent CLIs). Each VU iteration picks a row | approximated. Say the distribution follows the CLI default, not Gatling's round-robin. A Postman Dataset (`--dataset-distribution round-robin`) is the exact match |
19
+ | `.random()` | random | mapped |
20
+ | `.shuffle()` | random (with replacement) | approximated |
21
+ | `.queue()` / default | random; rows can repeat and the run never runs out | approximated. **Always** call out that uniqueness is lost: if the source relies on each row being used once (unique users, one-time tokens), the test will behave differently |
22
+ | `.shard()` | none | gap |
23
+ | `.batch()`, `.eager()`, `.transform(...)`, `.unzip()` | `eager` and `batch` are no-ops. `transform` → a pre-request script | approximated |
24
+
25
+ ## `feed(...)` calls (`feed:*`)
26
+ `feed(f)` at the start of the scenario → each iteration gets a row, and the columns become variables `{{column}}`. Mapped, with `target: "data:data/<file>"`.
27
+ `feed(f, n)` (several records at once) → gap.
28
+
29
+ **One data source, two inventory ids.** The inventory emits a `feeder:*` id (the source + its distribution strategy) **and** a `feed:*` id (the binding that hands a row to each iteration), both describing one logical data file. Account for them split by concern: the `feed:*` id is **mapped** (`target: data:data/<file>` — row → `{{column}}` variables), and the `feeder:*` id carries the **strategy** status (e.g. `.circular()` → **approximated**, "distribution follows the CLI default, not round-robin"; default `queue` → **approximated**, "uniqueness lost"). Both may point at the same `data:` target; that is expected, not a duplicate.
30
+
31
+ **Multiple feeders.** A scenario with more than one `feed(...)` (e.g. `feed(users).feed(products)`) has **no exact mapping**: `postman performance run` takes a single `--data-file` and the smoke run a single `-d`, so only one set of columns can bind. Do **not** emit two `--data-file` bindings and mark both mapped — one set would silently go unbound. Instead either: (a) **combine** the sources into one CSV/dataset by joining their columns, and document the changed row pairing/distribution in MIGRATION.md (**approximated**); or (b) map the primary feeder and record the others as **gaps**, naming which `{{columns}}` won't resolve. State the choice in the coverage table.
@@ -0,0 +1,88 @@
1
+ # Load model: injection, throttle, duration, assertions → `postman performance run`
2
+
3
+ ## What Postman supports (verify against the installed `postman performance run --help`; don't assume a version)
4
+ - **Closed model only.** `--vu-count N` virtual users each loop the collection until the run ends. There's no arrival rate and no target RPS.
5
+ - `--load-profile`: `fixed` (N the whole time), `ramp-up` (grows to N), `spike` (a burst to N), `peak` (rises to N, holds, falls). Each has a fixed shape; you can't set stage times from the CLI.
6
+ - `--duration` in whole minutes (≥ 1). `--pass-if` takes **one** condition: `less_than|greater_than|less_than_eq|greater_than_eq(avg|p90|p95|p99|error_rate|rps, value)`. `error_rate` is a percentage.
7
+ - Cloud runs need `--vu-count` ≥ 10. For local runs, any value ≥ 1 works.
8
+
9
+ ## Injection (`inj:*`)
10
+ **Closed steps: exact or near-exact.**
11
+ | Gatling | Flags | Status |
12
+ |---|---|---|
13
+ | `constantConcurrentUsers(n).during(d)` alone | `--vu-count n --load-profile fixed --duration ceil(d)` | mapped (approximated if d isn't whole minutes: say "rounded up") |
14
+ | `rampConcurrentUsers(a).to(b).during(d)`, then `constantConcurrentUsers(b).during(h)` | `--vu-count b --load-profile ramp-up --duration ceil(d+h)` | approximated: the ramp/hold ratio is fixed by the profile |
15
+ | `rampConcurrentUsers(a).to(b)` alone | `--load-profile ramp-up` | approximated |
16
+ | `incrementConcurrentUsers` (steps) | `ramp-up` to the final level | approximated: no stairs |
17
+
18
+ **Open steps: always approximated.** Gatling open users run the journey **once** and leave. Postman VUs loop. To keep the same load on the server, match the concurrency using Little's law:
19
+
20
+ > **VUs ≈ arrival rate (users/s) × journey time (s)**
21
+
22
+ - **Know whether the injector's number is a count or a rate — this is the most common mistake.** `constantUsersPerSec(r)` and `rampUsersPerSec(a).to(b)` are **rates** (users **per second**) — use them directly. `rampUsers(n).during(d)` and `atOnceUsers(n)` are a **total count** of users, not a rate: `rampUsers(50).during(60s)` is 50 users spread over 60 s = an arrival rate of `50/60 ≈ 0.83 users/s`, **not** 50/s. Convert a count to a rate (`n/d`) *before* Little's law. Skipping this step inflates `--vu-count` by the factor `d`.
23
+ - **Journey time** = the sum of pauses (use the mean for ranges, e.g. `pause(1,3)` → 2 s) + the expected response times. Use `0.3 s` per request if unknown, and say so — **but** if a path makes the latency evident (e.g. `/delay/1` ≈ 1 s, a Gatling request literally named "slow …"), use that value instead and note it. Pick one rule per conversion and state it, so two runs don't diverge.
24
+ - **Cloud VU floor.** If the computed `--vu-count` is below 10 and the user will run on **cloud**, keep the computed value but add a one-line note that cloud requires ≥ 10 (so they raise it deliberately, knowing it increases load). For **local** runs any value ≥ 1 is fine. Never silently inflate the count to clear the floor.
25
+ - **`pace(d)` is a floor, not an addend.** `pace` forces each iteration to take *at least* `d`; it does **not** add to the request/pause time, it subsumes it. So when a scenario has `pace(d)`, the journey time is `J = max(d, pauses + response times)`, **not** `pauses + response times + d`. Adding `pace` as a term overstates `J` and inflates `--vu-count`. Show the `max(...)` in MIGRATION.md.
26
+ - **A loop inside the per-user journey changes what `J` is.** If the open-model journey wraps a `during(t) { … }` / `forever { … }` loop (often with `pace`), each arriving user stays busy for the **whole loop duration**, so `J` is that duration, not one pass. Example: `constantUsersPerSec(20)` over a journey of `during(5.minutes){ pace(5s).exec(...) }` → `J = 300 s`, so `VUs ≈ 20 × 300 = 6000`, not `20 × (one 5 s iteration)`. This is usually a very large number — **do not silently pick the one-iteration reading**; compute both, put the arithmetic and the chosen reading in MIGRATION.md → Load model, and tell the user to confirm, because it swings `--vu-count` by orders of magnitude. A finite closed cap (`maxDuration`, cloud VU limits) may force you lower; say so.
27
+ - Round up to `--vu-count` ≥ 1. **Do not bump a sub-10 result up to 10 for cloud here** — the **Cloud VU floor** rule above governs that: keep the computed value and warn the user cloud needs ≥ 10, so load is never silently increased without their decision.
28
+
29
+ | Gatling | Flags |
30
+ |---|---|
31
+ | `constantUsersPerSec(r).during(d)` | `--vu-count ceil(r × J) --load-profile fixed --duration ceil(d)` |
32
+ | `rampUsersPerSec(a).to(b).during(d)` (then `constantUsersPerSec(b)…`) | `--vu-count ceil(b × J) --load-profile ramp-up`, with duration the sum of all steps |
33
+ | `atOnceUsers(n)` | a burst: `--load-profile spike --vu-count n` if it's the main (only sustained) step. **When it coexists with a ramp/constant step, the sustained step is the dominant shape** — use that profile and its Little's-law VU count; fold the burst in as the initial spike and note it. Only when the burst's `n` exceeds the sustained VU count does it set the peak (use the larger, and say so) |
34
+ | `rampUsers(n).during(d)` | **`n` is a total count, not a rate.** `n` users arrive evenly over `d`, so first convert to a rate `n/d` users/s, then `--vu-count ceil((n/d) × J)`, ramp-up. E.g. `rampUsers(50).during(60s)` → `50/60 ≈ 0.83/s`, **not** 50/s |
35
+ | `stressPeakUsers(n).during(d)` | `--load-profile peak --vu-count ceil(peak rate × J)` |
36
+ | `nothingFor(d)` | none. Add d to the explanation. Gap if it's the only step, otherwise approximated |
37
+ | `.randomized()` | none: gap (arrivals aren't randomised) |
38
+
39
+ When the chain has several steps, choose **one** profile for the dominant shape and put every step with its arithmetic in MIGRATION.md → Load model. Also write what the user should watch for: "Gatling measured N users arriving per second; Postman keeps V users busy. Compare throughput (rps) with your Gatling report rather than user counts."
40
+
41
+ Example (show your working like this):
42
+ ```
43
+ Source: rampUsersPerSec(1).to(10).during(60s), then constantUsersPerSec(10).during(120s)
44
+ Journey: 2 requests × 0.3 s (assumed) + pause(1,2) mean 1.5 s = 2.1 s
45
+ Peak VUs: 10 users/s × 2.1 s = 21 → --vu-count 21
46
+ Duration: 60 + 120 = 180 s = 3 min → --duration 3
47
+ Profile: ramp-up (one ramp then a hold; the ramp/hold split is fixed by the profile, not 1:2)
48
+ ```
49
+
50
+ Count-based injectors — convert the count to a rate FIRST (this is the step that most often goes wrong):
51
+ ```
52
+ Source: atOnceUsers(5), then rampUsers(50).during(60s)
53
+ Rate: rampUsers(50).during(60s) = 50 users over 60 s = 50/60 ≈ 0.83 users/s ← NOT 50/s
54
+ Journey: 3 requests × 0.3 s (assumed) + pause 0.5 s × 3 + /delay/1 ≈ 1 s = ~3.7 s
55
+ Sustained VUs: 0.83 users/s × 3.7 s ≈ 3.08 → 4
56
+ Burst: atOnceUsers(5) = 5 users at once; 5 > 4, so the burst sets the peak → --vu-count 5
57
+ Duration: 60 s = 1 min → --duration 1 ; Profile: ramp-up
58
+ ```
59
+ (Reading `rampUsers(50)` as 50 users/s here would give `50 × 3.7 ≈ 185` VUs — ~40× too high.)
60
+
61
+ ## Throttle (`throttle:*`) and max duration
62
+ - `throttle(reachRps(r).in(t), holdFor(h))`: there's no RPS cap. **Gap**, and say that Postman won't hold throughput at r. The user can lower `--vu-count` to get near it.
63
+ - `maxDuration(d)`: `--duration` is already a hard stop. Mapped to `run:--duration` if d ≥ the computed duration. Otherwise use d, and approximate.
64
+
65
+ ## Assertions (`assert:*`)
66
+ The CLI takes **at most one** `--pass-if`. Pick it by this **strict priority — this overrides the order the assertions appear in the source**:
67
+ 1. a global error-rate assertion (`failedRequests`/`successfulRequests` → `error_rate`)
68
+ 2. a global latency percentile (`p95`, then `p99`, then `p90`)
69
+ 3. a global `mean` (`avg`) or `requestsPerSec` (`rps`)
70
+
71
+ Walk the list top-down and take the **first category that a source assertion matches**, even if a different assertion came first in the source. Example: a simulation with `responseTime().percentile(95).lt(800)` **and** `failedRequests().percent().lt(1)` → the `--pass-if` is `less_than(error_rate, 1)` (category 1 wins), and the p95 assertion is the **gap**, not the other way round.
72
+
73
+ All non-chosen assertions are **gap**, reason "the CLI takes one pass condition". Suggest adding them as run criteria in the app, where several aggregate checks are allowed.
74
+
75
+ **If no source assertion matches any category** (e.g. only `forAll`/`details(...)` per-request assertions, or only `responseTime().max()` which has no CLI metric), emit **no `--pass-if` flag at all** — this is correct and expected, not a defect. Record every assertion as a gap and tell the user to add run criteria in the app. A `run.sh` with zero `--pass-if` is valid.
76
+ | Gatling | `--pass-if` | Notes |
77
+ |---|---|---|
78
+ | `global().failedRequests().percent().lt(x)` | `less_than(error_rate, x)` | approximated (see error-rate note below) |
79
+ | `global().failedRequests().count().is(0)` | `less_than_eq(error_rate, 0)` | approximated (percentage instead of count; + error-rate note) |
80
+ | `global().successfulRequests().percent().gt(x)` | `less_than(error_rate, 100 - x)` | approximated (see error-rate note below) |
81
+ | `global().responseTime().percentile(95).lt(ms)` (Scala `percentile3`) | `less_than(p95, ms)` | mapped (also p90, p99. Other percentiles → the nearest, approximated) |
82
+ | `global().responseTime().mean().lt(ms)` | `less_than(avg, ms)` | mapped |
83
+ | `global().responseTime().max()` / `min()` / `stdDev()` | none | gap |
84
+ | `global().requestsPerSec().gt(r)` | `greater_than(rps, r)` | mapped |
85
+ | `forAll…` / `details("group", "req")…` | none (no per-request criteria) | gap |
86
+ | `lte` / `gte` | `less_than_eq` / `greater_than_eq` | — |
87
+
88
+ **Error-rate note (why failed/successful-request mappings are approximated).** Gatling's `failedRequests` counts a request as failed when a **check** fails, even on an HTTP 200. Postman's `error_rate` counts transport/runtime errors and may **not** count a request that returned a response but failed a translated `pm.test` (behaviour differs by runner). So a batch of 200s that all fail a JSON check would fail Gatling's gate but can **pass** `less_than(error_rate, …)`. Map `failedRequests`/`successfulRequests` → `error_rate` as **approximated**, say so in MIGRATION.md, and note it's a weaker gate than Gatling's (plus any hosted-runner limitation). If the failure the user cares about is a check, a latency percentile or `rps` criterion may be the more faithful gate.
@@ -0,0 +1,91 @@
1
+ # Mapping: requests, EL, checks, protocol, flow, custom code
2
+
3
+ ## Protocol settings (`proto:*`)
4
+ | Gatling | Postman | Status |
5
+ |---|---|---|
6
+ | `.baseUrl("https://h")` | collection variable `baseUrl`; requests use `{{baseUrl}}/…` | mapped |
7
+ | `.baseUrls(a, b)` | first one as `baseUrl`; note the round-robin | approximated |
8
+ | `.header(k, v)`, `.headers(map)` | header on every request (or collection pre-request `pm.request.headers.upsert`) | mapped |
9
+ | `.acceptHeader`, `.contentTypeHeader`, `.userAgentHeader`, `.authorizationHeader` … | the matching header on every request | mapped |
10
+ | `.basicAuth(u, p)` | collection auth `basic`, with `{{user}}` / `{{password}}` from the environment | mapped |
11
+ | `.shareConnections()` | none. The runtime manages connections | gap (no-op; performance may differ) |
12
+ | `.enableHttp2()` | none. HTTP/1.1 only | gap |
13
+ | `.inferHtmlResources()` | none. Embedded resources are not fetched | gap |
14
+ | `.disableCaching()`, `.disableFollowRedirect()`, `.maxConnectionsPerHost()` | none, or the request setting `followRedirects: false` | gap / mapped for redirects |
15
+ | `.warmUp(url)` | none | gap |
16
+
17
+ If a header value is a secret (an inventory `secret` item, or a name like `x-api-key` or `authorization` with a literal), use `{{<camelCaseName>}}` and add an empty environment key.
18
+
19
+ ## Requests (`req:*`)
20
+ - Name: keep the `http("…")` name exactly.
21
+ - Method and path: `.get("/p")` → `GET {{baseUrl}}/p`. An absolute URL stays absolute.
22
+ - `.queryParam(k, v)` → a URL query param. `.formParam(k, v)` → `body.mode: urlencoded`. `.body(StringBody("…")).asJson()` → `body.mode: raw`, `options.raw.language: json`, plus header `Content-Type: application/json`.
23
+ - `.header(k, v)` on a request → that request's header.
24
+ - `ElFileBody("x.json")` / `RawFileBody` → inline the file contents into the raw body (translate EL inside it).
25
+
26
+ ## EL → Postman variables
27
+ | Gatling EL | Postman |
28
+ |---|---|
29
+ | `#{name}` | `{{name}}` |
30
+ | `#{randomUuid()}` | `{{$guid}}` |
31
+ | `#{randomInt()}` / `#{randomInt(a,b)}` | `{{$randomInt}}` (0–1000, approximated), or a pre-request script for a range |
32
+ | `#{randomLong()}` | a pre-request script, stored in a variable |
33
+ | `#{currentTimeMillis()}` | `{{$timestamp}}` is seconds, so use a pre-request script: `pm.variables.set('now', Date.now())` |
34
+ | `#{name.size()}`, `#{name(0)}`, `#{name.random()}` | a pre-request script over the parsed variable |
35
+ | `#{name.jsonStringify()}` | a pre-request script `JSON.stringify(...)` |
36
+ | `#{i}` from `repeat(n, "i")` | see Flow → loops |
37
+
38
+ Every `{{var}}` must be defined somewhere: the environment, collection variables, a data-file column, or set by an earlier script. The self-verify step checks this.
39
+
40
+ ## Checks (`check:*`) → test script
41
+ Put the checks in the request's `test` event, in source order. Name each `pm.test` after what it asserts.
42
+ | Gatling | Test script |
43
+ |---|---|
44
+ | `status().is(200)` (Kotlin: `shouldBe`) | `pm.test('status is 200', () => pm.response.to.have.status(200));` |
45
+ | `status().in(200, 204)` | `pm.expect([200,204]).to.include(pm.response.code)` |
46
+ | `jsonPath("$.a.b").is("x")` | `pm.expect(pm.response.json().a.b).to.eql('x')`. Gatling compares as a string unless `ofInt()` etc. is used, so compare `String(v)` when the source is a string literal |
47
+ | `jsonPath(...).isEL("#{v}")` | compare with `pm.variables.get('v')` (see **Saved variables scope**: read with `pm.variables.get`, write with `pm.collectionVariables.set`) |
48
+ | `jsonPath(...).exists()` / `.notExists()` | `pm.expect(value).to.not.be.undefined` / `.to.be.undefined` |
49
+ | `jsonPath(...).saveAs("v")` | `pm.collectionVariables.set('v', value)`, and also a `pm.test` that the value exists. **approximated**: Gatling marks the request failed on a missing extraction but, by default, **continues** the scenario — so do **not** add `pm.execution.setNextRequest(null)` here; that would skip the rest of the iteration even though the source doesn't. Stop early only when the source wraps this in `exitHereIfFailed`/`exitBlockOnFail` (see Flow control); if a later request needs to know the extraction failed, track that in a variable. Manifest target: the request's test script (`collection:<req> › test: saveAs v`), **not** `collection-variable:` — the value is script-set, not a declared collection variable |
50
+ | `jmesPath("a.b")` | same as jsonPath, using a JS property path |
51
+ | `regex("p").saveAs("v")` | `const m = pm.response.text().match(/p/); pm.test(..., () => pm.expect(m).to.not.be.null); pm.collectionVariables.set('v', m && m[1] !== undefined ? m[1] : m && m[0])`. Gatling saves group 1 if there is one, otherwise the whole match |
52
+ | `header("h").is(v)` | `pm.expect(pm.response.headers.get('h')).to.eql(v)` |
53
+ | `bodyString().is/contains` | `pm.response.text()` |
54
+ | `substring("s")` | `pm.expect(pm.response.text()).to.include('s')` |
55
+ | `responseTimeInMillis().lt(n)` | `pm.expect(pm.response.responseTime).to.be.below(n)` |
56
+ | `css(...)` / `xpath(...)` | cheerio (`require('cheerio')`) for CSS if the selector is simple. XPath → gap |
57
+ | `checkIf(cond)` | wrap the assertion in an `if` that translates the condition. Approximated if the condition is custom code |
58
+
59
+ **Implicit status check (`implicit-status:*`).** When a Gatling request declares no `status()` check, Gatling still fails it unless the status is 2xx or 304. Add `pm.test('status is 2xx/304 (Gatling implicit check)', () => pm.expect(pm.response.code === 304 || (pm.response.code >= 200 && pm.response.code < 300)).to.be.true);`. Status: mapped.
60
+
61
+ **Saved variables scope.** For correlation, **write** with `pm.collectionVariables.set('v', value)` and **read** with `pm.variables.get('v')` (the general resolver sees collection scope, so the two are consistent — don't mix `pm.collectionVariables.get` and `pm.variables.get` for the same value). In a performance run each virtual user has its own variable scope, which matches Gatling's per-user `Session`. A value saved this way is set by a script at run time, so it is **not** a declared collection variable — in the MIGRATION.md coverage table a `saveAs` lands on the request's test script, not a `collection-variable`.
62
+
63
+ **Name collisions with feeder columns.** Data-file (`iterationData`) values outrank collection variables when `{{x}}` / `pm.variables.get('x')` resolves (see `postman:variables`). So if a `saveAs`/session key has the **same name as a feeder column**, `pm.collectionVariables.set('x', …)` will **not** override it — `{{x}}` still resolves to the feeder's value, unlike Gatling where the later session write wins. When a saved key collides with a feeder column, **rename** the saved key (or the column) and use the renamed key consistently across the extraction, any custom-code translation, and the request substitutions; note the rename in MIGRATION.md.
64
+
65
+ ## Scenario, groups (`scenario:*`, `group:*`)
66
+ - Scenario → the collection itself (name the collection after it). If `setUp` has several scenarios, convert the main one and list the others as a gap: one collection per run.
67
+ - `group("g")` → a folder named `g` containing the requests in the group. Status: approximated. Postman reports per-request metrics, and there's no folder-level (transaction) timing. Say so.
68
+
69
+ ## Flow control (`flow:*`)
70
+ | Gatling | Postman | Status |
71
+ |---|---|---|
72
+ | `exec` chain (sequential) | request order in the collection | mapped |
73
+ | `repeat(n) { … }` / `repeat(n, "i")` | Use **one** request with a `setNextRequest` loop + a counter collection variable, so the single leaf runs n times. **Never unroll into n copies that share the same name**: a Postman collection can't hold two request leaves with the same path — so the "copies named `name`" shortcut is invalid. The loop also keeps the metric name identical (which is what unrolling was for). Only unroll if the body genuinely differs per iteration, and then give each copy a **distinct** name and note in MIGRATION.md that per-iteration metrics are split across those names. For `repeat(n, "i")`, set the counter as `{{i}}` in the loop script. **When the body is a chain of several requests** (`repeat(3){ A; B }`), loop the **whole body**, not one leaf: keep A and B as distinct requests and have the body's **last** request jump back to the **first** (`pm.execution.setNextRequest('A')`) while the counter < n, initialising/resetting the counter on first entry — so the order is A,B,A,B,A,B. A single-leaf loop only models a single-request body | approximated (explain the loop/metrics effect) |
74
+ | `during(d) { … }` / `forever { … }` wrapping the main journey | The VU loop itself: a VU repeats the collection until the run ends | approximated (Gatling users in open models start and stop. Here VUs loop) |
75
+ | `asLongAs(cond)`, `doWhile` | `setNextRequest` loop with a condition in the test script | approximated |
76
+ | `doIf(cond) { r }` | in the pre-request of `r`: `if (!cond) pm.execution.skipRequest()`. The condition is translated to JS (session attribute → `pm.variables.get`) | **mapped** if the condition reduces to a simple equality/comparison on a session value — *even when written as a lambda* (e.g. `session -> session.getString("tier").equals("gold")` → `pm.variables.get('tier') === 'gold'`). **approximated** only if it calls out to logic you can't express as a plain JS comparison (library calls, computation, external state). A lambda by itself does not force "approximated"; judge the body |
77
+ | `doIfOrElse`, `doSwitch`, `randomSwitch`, `roundRobinSwitch` | `skipRequest` logic per branch. `randomSwitch` percentages via `Math.random()` in a pre-request | approximated |
78
+ | `tryMax(n) { r }` | none. A request that fails isn't retried | gap. Suggest checking the error rate instead |
79
+ | `exitHereIfFailed` | in the test script: `if (pm.response.code >= 400) pm.execution.setNextRequest(null)` — ends the whole iteration | approximated (only stops on status failure, not on any failed check, unless you track that in a variable) |
80
+ | `exitBlockOnFail { … }` | exits **only its block**, then resumes after it — so on failure jump to the **first request after the block** (`pm.execution.setNextRequest('<next>')`), **not** `null`. Ending the iteration would skip requests/cleanup that sit outside the block | approximated (status-failure only unless you track check failures in a variable) |
81
+ | `stopLoadGenerator` / `crashLoadGenerator` | none | gap |
82
+
83
+ ## Custom code (`code:*`)
84
+ `exec(session -> …)` (Java), `exec { session -> … }` (Kotlin), `exec(session => …)` (Scala, JS):
85
+ - Translate it to a **pre-request script** on the next request.
86
+ - `session.set("k", v)` → `pm.collectionVariables.set('k', v)`
87
+ - `session.getString("k")` → `pm.collectionVariables.get('k')`
88
+ - `session.userId()` → `pm.info.iteration`, as an approximation
89
+ - `UUID.randomUUID()` → `crypto.randomUUID()` if available, otherwise `pm.variables.replaceIn('{{$guid}}')`
90
+ - Always prefix it with `// REVIEW: translated from Gatling custom code (<id>)`. Status: approximated. List it in MIGRATION.md → Approximations, with the original code.
91
+ - JVM library calls you can't translate (JDBC, crypto libraries, file IO) → gap.
@@ -0,0 +1,31 @@
1
+ # What can't be carried over, and how to say so
2
+
3
+ Postman performance runs are **HTTP(S) only** (GraphQL over HTTP and mTLS work). Load settings are the CLI flags in `injection.md`.
4
+
5
+ ## Always a gap
6
+ | Construct | Inventory kind | Wording for MIGRATION.md |
7
+ |---|---|---|
8
+ | `ws(...)`, `sse(...)` | `non_http_step` | WebSocket/SSE steps aren't part of Postman performance runs. They were removed from the load test. Test the socket flow separately (Postman's WebSocket client works for functional checks). |
9
+ | `grpc(...)`, `jms(...)`, `mqtt(...)`, `kafka(...)`, `amqp(...)` | `non_http_step` | This protocol isn't supported under load in Postman. These steps were removed. |
10
+ | `rendezVous(n)` | `timing` | There's no way to synchronise virtual users. Users won't wait for each other. |
11
+ | `pace(d)` | `timing` | There's no iteration pacing: VUs start the next iteration immediately. Add an explicit delay in the last request's test script only if the pacing matters (`setTimeout` isn't reliable under load, so prefer lowering `--vu-count`). |
12
+ | `throttle(...)` | `throttle` | There's no RPS cap. Throughput isn't limited to the Gatling target. |
13
+ | `tryMax(n)` | `flow` | Failed requests aren't retried. |
14
+ | `stopLoadGenerator`, `crashLoadGenerator` | `flow` | There's no scripted run abort. Use `--pass-if` to fail the run afterwards. |
15
+ | `.randomized()` on injection | `injection` | Arrivals aren't randomised. |
16
+ | `.enableHttp2()`, `.inferHtmlResources()`, `.shareConnections()` | `protocol_setting` | See `mapping.md`. |
17
+ | second and later assertions | `assertion` | The CLI takes one `--pass-if`. |
18
+ | `forAll` / `details(...)` assertions | `assertion` | There are no per-request pass criteria. |
19
+ | JDBC/Redis feeders | `feeder` | Export to CSV first. |
20
+ | extra scenarios in `setUp` | `scenario` | One collection per run. Convert them separately and run them in parallel. |
21
+
22
+ ## Pauses
23
+ `pause(d)` and `pause(min, max)` (`timing`) have no first-class think time. Always a **gap**; how it affects the result depends on the injection model:
24
+ - **Open model:** the pause is part of the journey time, so it already feeds the Little's-law `--vu-count` arithmetic in `injection.md`. Dropping the think time alone (keeping the computed VU count) means VUs send faster than Gatling users did. Say which metric (rps) to compare with the Gatling baseline.
25
+ - **Closed model:** there is no journey-time arithmetic to fold into — the VU count is the source concurrency directly. The pause is simply dropped: VUs loop with no think time, so they issue more requests per VU than Gatling did. Record it as a gap and tell the user throughput (rps) will be higher than Gatling's; they can lower `--vu-count` to compensate.
26
+ - If the user explicitly wants think time: a pre-request script with a busy-wait isn't acceptable under load. Record it as a gap, and suggest lowering `--vu-count` in proportion.
27
+
28
+ ## Wording rules
29
+ - One bullet per id: `` `<id>` ``, what changed, why, what to do. Keep it to 1–2 lines.
30
+ - Never write "not supported yet" or promise a roadmap. State today's behaviour.
31
+ - If the gap changes the results (pauses, pace, throttle, queue feeders, open-model injection), say which metric to compare with the Gatling baseline.
@@ -9,7 +9,7 @@ MCP exposes workspace resources directly to an agent. Existing collections,
9
9
  specs, examples, and environments provide maintained API knowledge; fetching
10
10
  the relevant entity preserves that context when a local CLI workflow is not
11
11
  available. For repository edits and Git review, prefer the local workflows in
12
- **api-engineer** and **bootstrap** when available.
12
+ **api-engineer** when available.
13
13
 
14
14
  Each tool definition exposed to the model consumes context whenever it is
15
15
  included in a request. Prefer the smallest available toolset that covers the
@@ -47,8 +47,8 @@ postman environment var set api_key --secret-id <secret-id> --vault-id <vault-id
47
47
 
48
48
  The environment then carries the vault/secret IDs rather than the value.
49
49
  Secret resolution and permission to view the secret are distinct; a teammate's
50
- ability to run a request need not expose the credential to them. Use
51
- **bootstrap** when the vault operation needs unresolved authentication.
50
+ ability to run a request need not expose the credential to them. Vault
51
+ operations require a signed-in CLI.
52
52
 
53
53
  ## Verification
54
54
 
@@ -23,7 +23,7 @@ how to use it:
23
23
  to fire it again.
24
24
 
25
25
  The command is singular, and every subcommand needs a signed-in session. Run what you need,
26
- and read `bootstrap` only if a command fails with a setup error such as `Authentication required`.
26
+ and sign in only if a command fails with a setup error such as `Authentication required`.
27
27
  When all you have is the URL, its subdomain is the webhook id.
28
28
 
29
29
  For capture or inspection alone, create or reuse a webhook and inspect its
@@ -0,0 +1,53 @@
1
+ ---
2
+ name: workspaces
3
+ description: Use when initializing Postman in a repository, binding it to a Postman workspace, interpreting what `postman init` did, or when a CLI command fails because the plan, credits, or usage quota don't allow the operation. Treats the workspace as the place where a service's API work and its evidence live, kept separate from the services it depends on. Use for requests such as "set up Postman in this repo" or "connect this repo to our workspace".
4
+ ---
5
+
6
+ # Workspaces
7
+
8
+ A Postman workspace is where a service's API work happens and where its context
9
+ builds up: the contracts, collections, environments, and mocks, plus the runs
10
+ and results that show what was checked. It lives in Git beside the code, works
11
+ the same on a laptop or a build agent, and is shared when the work should be
12
+ seen. People use it to audit what was done, by whom, on whose behalf, and with
13
+ what result. Agents use it to read verified work instead of re-deriving it.
14
+
15
+ 1. `postman init` is the starting point. It gives the repository its place of
16
+ work: Postman files beside the code and, when that is the intent, a
17
+ workspace bound to them. `postman status` shows whether that is already
18
+ done, so init runs only when it adds something. Init is safe to repeat and
19
+ reports what it did: a binding it marks as inferred is a candidate contract
20
+ to confirm, and a partial result, with local files written but no workspace
21
+ created, is progress to inspect rather than redo.
22
+ 2. One workspace holds the context of one service: one Git repository, or in a
23
+ monorepo one remote URL and the subpath of one service.
24
+ `.postman/resources.yaml` records that owner, so every skill, CI job, and
25
+ teammate resolves the same context. A confirmed binding is worth reusing;
26
+ replacing it sends the work, and its evidence, to the wrong service.
27
+ 3. Other services keep their own workspaces. Their collections, environments,
28
+ and mocks come in as dependencies through **api-discovery**, so this
29
+ workspace reuses what another team maintains without taking it over. The
30
+ context graph links the two, which is how a change on one side reaches work
31
+ on the other.
32
+
33
+ ## Claiming the work
34
+
35
+ An agent can start in a guest session, and the work it does there is kept.
36
+ When free credits or a plan limit stop an operation, retrying won't help; the
37
+ person claiming the work is what lets it continue. Wording such as "usage limit
38
+ exceeded", "usage quota has been exceeded", a limit "reached", "breached" or
39
+ "exceeded" for the account, team, or plan, or an operation that "does not allow
40
+ more than" or "requires at least" a stated amount signals one. A temporary rate
41
+ limit or a fixed per-resource validation cap does not. Check which session the
42
+ CLI is using:
43
+
44
+ - Guest session: tell the user, in its own paragraph, that signing up for a
45
+ Postman account claims the work done so far and adds the credits needed to
46
+ continue. Offer to start signup for them; the CLI can produce a link to hand
47
+ over and waits while they finish in the browser.
48
+ - Signed-in account: tell the user, in its own paragraph, that they need to
49
+ upgrade their plan at https://postman.postman.co/settings/team/billing.
50
+
51
+ ## Verification
52
+
53
+ Report the binding in place and what `postman init` did.