issue-190: Implement the penpot_list_projects tool (teams and projects) with unit tests #223
No reviewers
Labels
No labels
bug
chore
documentation
enhancement
feature
ready
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
david/pi-extensions-and-skills!223
Loading…
Reference in a new issue
No description provided.
Delete branch "feature/issue-190/implement-the-penpot-list-projects-tool-teams-and"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
190: Implement the penpot_list_projects tool (teams and projects) with unit tests
Closes #190 — 190.
Generated in 9m 26s.
Review findings
I'll start by exploring the penpot extension to understand the current state.Now I understand the structure. Let me check the git status and the current test baseline.Now let me apply the fixes to
projects.ts:Now the test file:Now let me add the new tests for bounded concurrency, cancellation, and malformed teams:Now theindex.test.tsregistration-order fix:All fixes are in place and the suite is green. Final verification:# Review Findings — 190Summary
All eight findings were addressed in
extensions/penpot/src/tools/projects.ts,extensions/penpot/src/tools/projects.test.tsandextensions/penpot/src/index.test.ts, taking the penpot suite from 78 to 81 tests (13 inprojects.test.ts, all passing, stable across repeated runs). The Critical RPC-contract fix sends the kebab-caseteam-idquery key (TEAM_ID_QUERY_KEY) and the stub now asserts that spelling; the High fix parses the real{type, isOwner, isAdmin, canEdit}permission record (rendering only granted flags asowner/admin/edit, never thetypesentinel) with fixtures updated to the captured camelCase shape; the Medium abort finding is handled bythrowIfAbortedguards inexecute/attachProjectsplus asignal.abortedrethrow, with new tests for mid-fan-out cancellation and for the bounded fan-out (six teams, observed max 4 in flight, team order preserved); the Low findings are covered by filteringget-teamsentries without an id, addingadditionalProperties: falsewith the exactteam_idspelling in the parameter description, and asserting registration membership rather than order. Gates: the repo has no build/lint/typecheck command (no eslint/biome/tsconfig,node_modules/.binhas neithertscnor a linter), so onlynpm testwas run: 208 tests, 207 pass, 1 fail — the sole failure (scripts/local-install.test.mjs:1875,extensions/mongodb/index.tsmissing frompackage.jsonpi.extensions) is pre-existing onmain, untouched by this diff and unrelated to this issue (it was also present before remediation: 205/204/1). Residual risk: noPENPOT_URL/PENPOT_TOKENis configured in this environment, so the issue's live check ("confirm no validation error from the server") could not be run; the Critical fix follows the reviewer's source-level analysis of Penpot 2.17 rather than an observed live response.Critical
extensions/penpot/src/tools/projects.ts:209—query: { teamId: team.id }sends the query keyteamId(camelCase), but on Penpot 2.17get-projectsdeclares[:map {:title "get-projects"} [:team-id ::sm/uuid]](backend/src/app/rpc/commands/projects.clj), and GET query params are keywordized verbatim by yetti (parse-query-datadefaultkey-fn keyword), with no camelCase→kebab normalisation inwrap-params-validation(sm/decoder schema sm/json-transformer— penpot's transformer only adds a:map-ofcompiler). SoGET /api/rpc/command/get-projects?teamId=<uuid>fails param validation ("missing required key team-id") and every team is returned aserror: … could not be listedwhile the tool still reports success. Note the live instance's own OpenAPI documentsget-projectsasPOSTwith a JSON body{"teamId": …}(body keys are kebabed byjson/read-kebab-key), which is why the camelCase name works for a body but not for a GET query. Suggested fix: send the kebab query key —query: { "team-id": team.id }(or switch to{ method: "POST", body: { teamId: team.id } }as the OpenAPI documents) — and updateprojects.test.ts:59,:159,:247,:294to read/assertteam-idso the stub asserts the real contract; then run the issue's live check ("confirm no validation error from the server") before merging.High
extensions/penpot/src/tools/projects.ts:126— thepermissionsrecord branch oftoStringListtreats the server's permission map as{permission: boolean}, butget-teamsreturnsprocess-permissionsoutput:{:type :membership, :is-owner …, :is-admin …, :can-edit …}(backend/src/app/rpc/commands/teams.clj), serialized camelCase as{"type":"membership","isOwner":…,"isAdmin":…,"canEdit":…}. The truthy-key filter therefore emits the non-permission sentineltypeand drops false flags, so an owner seesPermissions: canEdit, isAdmin, isOwner, typeand a read-only member sees exactlyPermissions: type. This misreports the acceptance criterion "the caller's permissions". Suggested fix: parse the known shape explicitly (droptype, render only real capability flags, e.g.edit,admin,owner, orcanEdit=true, isAdmin=false, …) instead of a generic truthy-key map, and update theprojects.test.ts:141fixture to the real camelCase shape (the current{"can-edit": true, "can-read": true}invents acan-readpermission and hides the sentinel).Medium
extensions/penpot/src/tools/projects.ts:212— a caller cancellation is swallowed: the client maps anAbortErrorto atransportPenpotError(client.tsdescribeFetchError),isGlobalFailure(line 185) only treatsconfig/401as global, so an aborted fan-out keeps issuing requests on the already-aborted signal and returns a normal "success" result where every team carries athe request was abortederror instead of propagating the cancellation. Suggested fix: inattachProjects(and before continuing the fan-out), checksignal?.aborted(or give aborts their own error category) and rethrow, soexecutefails fast on cancellation; add a test that aborts mid-fan-out and asserts the tool throws rather than returning a listing.extensions/penpot/src/tools/projects.test.ts:133— no test exercises the bounded fan-out.MAX_CONCURRENT_TEAM_REQUESTS(projects.ts:42) is the only guard against an unbounded request burst and the issue explicitly requires it, yet every test uses at most two teams, so the concurrency logic can regress silently. Suggested fix: add a test with > 4 teams whose stub records in-flight count and start order, asserting the observed maximum never exceeds 4 and that result order matches team order.extensions/penpot/src/tools/projects.test.ts:141— the team fixtures are not the shape Penpot actually returns (hand-written kebab-case permission keys including a non-existentcan-read, andfeatures/permissionsnever exercised in their real{type, isOwner, isAdmin, canEdit}form), so the suite gives false confidence about output correctness. Suggested fix: replace the fixtures with a captured realget-teamspayload (camelCase keys,typesentinel) and assert the rendered permission text; this is the same fix as the High finding above and should land with it.Low
extensions/penpot/src/tools/projects.ts:149— aget-teamsentry without anidbecomesid: "", which then triggersget-projectswithteam-id=(empty) and surfaces a server validation error as that team's error. Suggested fix: filter out entries with no usable id (or set the entry'serrorlocally to a malformed-team message) rather than issuing a doomed request.extensions/penpot/src/tools/projects.ts:49— the permissive schema (noadditionalProperties: false) means a caller passingteamId— the exact name Penpot's own OpenAPI uses for this parameter — is silently ignored and returns the full team listing instead of an error, which is a plausible slip for a model reading the API docs. Suggested fix: keep the schema additive but reject unknown keys withadditionalProperties: false(aswhoami.tsdoes), or acceptteamIdas an alias; at minimum state the exact spelling in the parameter description.extensions/penpot/src/index.test.ts:74— the registration test asserts the exact order of registered tool names, coupling the test to registration order rather than membership. Suggested fix: assert the set of names (e.g. sort beforedeepEqual, or assertincludesfor each expected name) so reordering registrations doesn't produce a spurious failure.Documentation
Updated files: