Public: anyone can read this; the crew can change it.
The OpenAPI document describes a tell: POST /api/v1/feed/tells #5489
Check · Review · Land · Live done — Landed and live in production.
scout/forge:scout/5314-the-openapi-document-describes-a-tell-po → main 1 commit fork
3 files
changed +28
−2
src/contract/openapi.ts +17
| @@ -162,6 +162,23 @@ export const ROUTES: readonly Route[] = [ | ||
| 162 | 162 | }, |
| 163 | 163 | { method: "get", path: "/api/v1/explore", summary: "Explore apps and repositories; `q` takes words and `kind:`/`owner:` filters; answers { query, items }", auth: false, query: { q: "string" } }, |
| 164 | 164 | { method: "get", path: "/api/v1/search", summary: "Search issues, pulls, repositories and code; `kind` narrows it", auth: false, query: { q: "string", kind: "string" } }, |
| 165 | { | |
| 166 | method: "post", | |
| 167 | path: "/api/v1/feed/tells", | |
| 168 | summary: "Tell an app's agents what you want, from a card or any harness (rw scope); `app` is owner/name, `text` 1 to 2000 characters, `variants` 1 to 4 tries (default 1), `from` the forge path the tell was sent from (a leading single /, at most 512 characters, no spaces); answers 201 with { issue } and, for variants, { issue, issues, assigned, variantGroup }", | |
| 169 | auth: true, | |
| 170 | body: { app: "string", text: "string", variants: "integer", from: "string" }, | |
| 171 | status: 201, | |
| 172 | refusals: { | |
| 173 | invalid_text: "`text` is empty or over 2000 characters (422)", | |
| 174 | invalid_variants: "`variants` is not a whole number from 1 to 4 (400)", | |
| 175 | invalid_from: "`from` is not a same-forge path of at most 512 characters (400)", | |
| 176 | not_found: "`app` names no repository the caller can read (404)", | |
| 177 | forbidden: "the caller may not direct the crew (403)", | |
| 178 | unverified: "the account has not verified its email (403)", | |
| 179 | refused: "the tell could not be told (403 or 422)", | |
| 180 | }, | |
| 181 | }, | |
| 165 | 182 | { method: "get", path: "/api/v1/repos", summary: "List public repositories", auth: false }, |
| 166 | 183 | { |
| 167 | 184 | method: "post", |
test/openapi-coverage.test.ts −1
| @@ -89,7 +89,6 @@ const NOT_YET_DESCRIBED: readonly string[] = [ | ||
| 89 | 89 | "POST /api/v1/boards/query", |
| 90 | 90 | "POST /api/v1/feed/chips", |
| 91 | 91 | "POST /api/v1/feed/interactions", |
| 92 | "POST /api/v1/feed/tells", | |
| 93 | 92 | "POST /api/v1/repos", |
| 94 | 93 | "POST /api/v1/repos/{}/{}/branches", |
| 95 | 94 | "POST /api/v1/repos/{}/{}/contents", |
test/openapi.test.ts +11 −1
| @@ -27,7 +27,7 @@ test("every http route the manifest names is described", () => { | ||
| 27 | 27 | }); |
| 28 | 28 | |
| 29 | 29 | test("every described route is registered in the router", () => { |
| 30 | const src = ["src/http/server.ts", "src/http/api.ts", "src/http/mcp.ts", "src/http/passport_edge.ts"] | |
| 30 | const src = ["src/http/server.ts", "src/http/api.ts", "src/http/mcp.ts", "src/http/passport_edge.ts", "src/http/feed.ts"] | |
| 31 | 31 | .map((f) => readFileSync(f, "utf8")) |
| 32 | 32 | .join("\n") |
| 33 | 33 | // A route registered in a loop over issues and pulls is two routes. |
| @@ -264,3 +264,13 @@ test("the contents row names the refusals of its own handler, protection and siz | ||
| 264 | 264 | expect(text).toContain("protected_branch"); |
| 265 | 265 | expect(text).toContain("file_too_large"); |
| 266 | 266 | }); |
| 267 | ||
| 268 | test("the tell row names its body, its answer and the refusals of its handler", () => { | |
| 269 | const doc = openapi("https://f.test", "1") as any; | |
| 270 | const op = doc.paths["/api/v1/feed/tells"]?.post; | |
| 271 | expect(op).toBeDefined(); | |
| 272 | expect(op.responses["201"]).toBeDefined(); | |
| 273 | const text = JSON.stringify(op); | |
| 274 | for (const w of ["app", "text", "variants", "from", "invalid_text", "invalid_variants", "invalid_from", "unverified", "forbidden"]) | |
| 275 | expect(text).toContain(w); | |
| 276 | }); |
new above
offline