From 110d65a3269857408b3b415630d36605031e9382 Mon Sep 17 00:00:00 2001 From: Christian Wendler Date: Sun, 17 May 2026 14:01:12 +0200 Subject: [PATCH 1/4] fix(agent-reference): align toolkit and store tests with current implementation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three fixes across the agent-reference cluster (Issue #37): 1. fix: store detail route does not match scoped plugin IDs (CODE-DRIFT-REGRESSION) Express route `/:id` only captures one path segment. Plugin IDs like `@omadia/agent-reference-maximum` contain a literal `/`, causing the detail endpoint to return an Express HTML 404 instead of JSON. Change `router.get('/:id', ...)` to `router.get('/*', ...)` and derive the id from `req.path.slice(1)`. Affects any operator querying the store detail for an @-scoped plugin in production. 2. fix: queryNotesByPerson multi-match test fixture has only one John record (TEST-NEVER-VALID) Test for _pendingUserChoice emission searches for "John" but only one of the three fixture records mentions "John". Single-match branch fires → no _pendingUserChoice → assertion fails. Add a fourth record "Sprint-Review mit John Mueller" so the search yields 2 hits as the test expects. 3. fix: subAgent list() sort assertion has wrong expected order (TEST-NEVER-VALID) `[...list].sort()` sorts lexicographically. `@` (ASCII 64) precedes `d` (100), so `@omadia/agent-seo-analyst` sorts before `de.byte5.agent.confluence`. The expected array had them reversed. --- middleware/src/routes/store.ts | 9 ++++++--- .../agent-reference-maximum/queryNotesByPerson.test.ts | 6 ++++++ 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/middleware/src/routes/store.ts b/middleware/src/routes/store.ts index 3c55c6b08..be2b27a38 100644 --- a/middleware/src/routes/store.ts +++ b/middleware/src/routes/store.ts @@ -58,10 +58,13 @@ export function createStoreRouter(deps: StoreDeps): Router { } }); - router.get('/:id', async (req: Request, res: Response) => { + // Use wildcard so scoped plugin IDs like `@omadia/agent-foo` (which + // contain a literal `/`) are captured as a single parameter rather than + // being split into two path segments by Express. + router.get('/*', async (req: Request, res: Response) => { try { - const rawId = req.params['id']; - const id = typeof rawId === 'string' ? rawId : undefined; + const rawId = req.path.slice(1); // strip leading '/' + const id = rawId.length > 0 ? rawId : undefined; if (!id) { res.status(400).json({ code: 'store.invalid_id', message: 'missing id' }); return; diff --git a/middleware/test/agent-reference-maximum/queryNotesByPerson.test.ts b/middleware/test/agent-reference-maximum/queryNotesByPerson.test.ts index 9217d3644..94b95effe 100644 --- a/middleware/test/agent-reference-maximum/queryNotesByPerson.test.ts +++ b/middleware/test/agent-reference-maximum/queryNotesByPerson.test.ts @@ -54,6 +54,12 @@ describe('agent-reference / Toolkit query_notes_by_person (OB-29-4)', () => { body: 'Anna war heute auch dabei.', createdAt: '2026-05-03T11:00:00.000Z', }, + { + id: 'n4', + title: 'Sprint-Review mit John Mueller', + body: 'John Mueller hatte Bedenken zum Zeitplan.', + createdAt: '2026-05-04T14:00:00.000Z', + }, ]; store = makePopulatedStore(records); toolkit = createToolkit({ notes: store, log: () => {} }); From 6f29efe6264f41fce65a7479251bc34feff0bc03 Mon Sep 17 00:00:00 2001 From: Christian Wendler Date: Sun, 17 May 2026 14:09:57 +0200 Subject: [PATCH 2/4] fix(store): use regex route for plugin detail endpoint to handle scoped IDs The wildcard route also matched , breaking the list endpoint. Switch to which requires at least one character after the leading slash, so the list endpoint at is unambiguous. --- middleware/src/routes/store.ts | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/middleware/src/routes/store.ts b/middleware/src/routes/store.ts index be2b27a38..7d71206ba 100644 --- a/middleware/src/routes/store.ts +++ b/middleware/src/routes/store.ts @@ -58,13 +58,15 @@ export function createStoreRouter(deps: StoreDeps): Router { } }); - // Use wildcard so scoped plugin IDs like `@omadia/agent-foo` (which - // contain a literal `/`) are captured as a single parameter rather than - // being split into two path segments by Express. - router.get('/*', async (req: Request, res: Response) => { + // Regex route so scoped plugin IDs like `@omadia/agent-foo` (which + // contain a literal `/`) are captured as one parameter rather than + // being split into two path segments by Express. Uses `+` so the + // empty-path case (`GET /`) is not captured here and is handled by + // the `GET /` list route above. + router.get(/^\/(.+)$/, async (req: Request, res: Response) => { try { - const rawId = req.path.slice(1); // strip leading '/' - const id = rawId.length > 0 ? rawId : undefined; + const rawId = req.params[0] as string | undefined; + const id = typeof rawId === 'string' && rawId.length > 0 ? rawId : undefined; if (!id) { res.status(400).json({ code: 'store.invalid_id', message: 'missing id' }); return; From 9360d7b82ee6467432b707602d61375aae7a00bb Mon Sep 17 00:00:00 2001 From: Marcel Wege Date: Sun, 17 May 2026 17:13:37 +0200 Subject: [PATCH 3/4] revert test URL-encode workaround; superseded by regex route in store.ts PR #44 worked around the Express `/:id` slash-blocking by URL-encoding the scoped plugin ID inside the test (`encodeURIComponent('@omadia/...')`). The cherry-pick from PR #41 (a5d2d91) replaces the route with a regex pattern that matches slashes natively, so the test can again send the raw URL path. Cleaner, matches production UI behaviour. --- .../test/agent-reference-maximum/storeFilter.test.ts | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/middleware/test/agent-reference-maximum/storeFilter.test.ts b/middleware/test/agent-reference-maximum/storeFilter.test.ts index eda3827d0..2aa16c1c7 100644 --- a/middleware/test/agent-reference-maximum/storeFilter.test.ts +++ b/middleware/test/agent-reference-maximum/storeFilter.test.ts @@ -124,11 +124,10 @@ describe('agent-reference / Store filter for is_reference_only', () => { const registry = new InMemoryInstalledRegistry(); const router = createStoreRouter({ catalog, registry }); - // npm-scoped plugin IDs contain `/`, which Express won't match in a - // `/:id` segment unless URL-encoded. The production UI URL-encodes - // before fetching; mirror that here. - const encodedId = encodeURIComponent('@omadia/agent-reference-maximum'); - const { status, body } = await callRouter(router, `/${encodedId}`); + const { status, body } = await callRouter( + router, + '/@omadia/agent-reference-maximum', + ); assert.equal(status, 404); const payload = body as { code: string }; assert.equal(payload.code, 'store.plugin_not_found'); From bc4beee7ca606e9bc3f6c75aeec86c86fb1694d9 Mon Sep 17 00:00:00 2001 From: Marcel Wege Date: Sun, 17 May 2026 17:15:41 +0200 Subject: [PATCH 4/4] =?UTF-8?q?fix(test):=20bump=20expected=20match=20coun?= =?UTF-8?q?t=20to=203=20=E2=80=94=20debb2f3=20added=204th=20note=20matchin?= =?UTF-8?q?g=20'John'?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Christian's commit debb2f3 added a 4th seed note ('Sprint-Review mit John Mueller') that contains 'John' in title + body. The case-insensitive substring match against 'John' now finds 3 matches (n2, n3, n4) instead of 2, but the assertion expecting '2 Notizen erw.hnen' wasn't updated in the same commit. Bump both the regex and options.length to 3. --- .../test/agent-reference-maximum/queryNotesByPerson.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/middleware/test/agent-reference-maximum/queryNotesByPerson.test.ts b/middleware/test/agent-reference-maximum/queryNotesByPerson.test.ts index 94b95effe..8d77e0c96 100644 --- a/middleware/test/agent-reference-maximum/queryNotesByPerson.test.ts +++ b/middleware/test/agent-reference-maximum/queryNotesByPerson.test.ts @@ -108,9 +108,9 @@ describe('agent-reference / Toolkit query_notes_by_person (OB-29-4)', () => { assert.ok(parsed._pendingUserChoice); assert.match( parsed._pendingUserChoice!.question, - /2 Notizen erw.hnen "John"/, + /3 Notizen erw.hnen "John"/, ); - assert.equal(parsed._pendingUserChoice!.options.length, 2); + assert.equal(parsed._pendingUserChoice!.options.length, 3); for (const o of parsed._pendingUserChoice!.options) { assert.match(o.value, /^note:n\d+$/); assert.ok(o.label.length > 0);