Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 17 additions & 10 deletions packages/plugin-chatbot/src/__tests__/agentListShapes.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,10 @@
* `GET /api/v1/ai/agents` shape tolerance (objectstack#4053).
*
* Two producers serve this route — the framework dispatcher's degraded fallback
* when no AI service is registered, and cloud's `service-ai` — and it is
* mid-migration onto the platform's declared `{ success: true, data }` envelope.
* when no AI service is registered, and cloud's `service-ai`. Both answer in the
* platform's declared `{ success: true, data }` envelope now (objectstack#4124,
* cloud#929); the unenveloped shapes below are back-compat for deployments from
* before that, since a console release is not pinned to the server it runs against.
*
* Why this file exists rather than a comment: an unrecognised shape here does not
* throw, warn, or log. It yields an empty list, and `useAiSurfaceEnabled` turns an
Expand All @@ -16,9 +18,10 @@
* no visible difference. The tests are the only thing standing between an envelope
* conversion on the server and the AI UI quietly vanishing for every user.
*
* Teaching the reader all three shapes BEFORE any producer converts is what lets
* the server side move on its own schedule instead of landing in lockstep with a
* console release.
* Teaching the reader every shape BEFORE any producer converted is what let the
* server side move on its own schedule instead of landing in lockstep with a
* console release — and it is why the two conversions could land in separate
* repos, as separate PRs, with nothing here to change.
*/

import { describe, expect, it } from 'vitest';
Expand All @@ -28,7 +31,7 @@ const ASK = { name: 'ask', label: 'Ask' };
const BUILD = { name: 'build', label: 'Build' };

describe('extractAgentList — every shape this route answers in', () => {
it('reads `{ agents }` — what both producers send today', () => {
it('reads `{ agents }` — a pre-conversion server this console still meets', () => {
expect(extractAgentList({ agents: [ASK, BUILD] })).toEqual([ASK, BUILD]);
});

Expand All @@ -38,13 +41,17 @@ describe('extractAgentList — every shape this route answers in', () => {

it('reads the declared envelope with `data` as the array', () => {
// The shape objectstack#3983 set the precedent for: `data` carries the
// payload directly. This is the variant that silently emptied the list.
// payload directly. Neither producer took it — objectstack#4053 settled on
// the relocation below — and this is the variant that would have silently
// emptied the list had one of them. Read anyway: if a producer ever drifts
// here, the console keeps working and the drift surfaces in that repo's own
// envelope pins rather than as a vanished AI surface.
expect(extractAgentList({ success: true, data: [ASK, BUILD] })).toEqual([ASK, BUILD]);
});

it('reads the declared envelope with `data: { agents }`', () => {
// The other plausible conversion — relocating the existing payload under
// `data` rather than flattening it. Both must read the same.
it('reads the declared envelope with `data: { agents }` — the shipped shape', () => {
// The conversion both producers landed: the declared payload RELOCATED under
// `data` rather than flattened. All four shapes must still read the same.
expect(extractAgentList({ success: true, data: { agents: [ASK, BUILD] } })).toEqual([ASK, BUILD]);
});

Expand Down
38 changes: 23 additions & 15 deletions packages/plugin-chatbot/src/useAgents.ts
Original file line number Diff line number Diff line change
Expand Up @@ -152,27 +152,35 @@ export function resolveDefaultAgentName(
/**
* Pull the agent list out of whatever `GET /api/v1/ai/agents` answered.
*
* This route is served by more than one producer and is mid-migration onto the
* platform's declared `{ success: true, data }` envelope
* (objectstack#4053), so three shapes have to read the same:
* This route is served by more than one producer, and the conversion onto the
* platform's declared `{ success: true, data }` envelope (objectstack#4053) has
* now landed on BOTH — the framework's degraded fallback in objectstack#4124,
* cloud's `service-ai` in cloud#929. Four shapes still read the same:
*
* { success: true, data: { agents } } what both producers serve today
* { success: true, data: [ … ] } the envelope flattened — no producer
* emits it; read so that one doing so
* fails loudly upstream, not here
* { agents: [ … ] } pre-conversion servers
* [ … ] a bare array
* { agents: [ … ] } today's shape, both producers
* { success: true, data: … } the declared envelope, whose `data`
* may be the array or `{ agents }`
*
* The last two are back-compat, not the current wire: a console release is not
* pinned to the server it runs against, so a deployment from before the
* conversion is ordinary. Do not read them as evidence the servers are
* unenveloped — they are not, and a "cleanup" that re-narrows this to the bare
* shape would break exactly the deployments it looks like it is following.
*
* The envelope is detected the way `ObjectStackClient.unwrapResponse` detects
* it — a **boolean** `success` — so the two agree on what counts as one.
*
* Reading the envelope BEFORE any producer emits it is deliberate, and it is the
* whole point of doing this ahead of the conversion. An unrecognised shape here
* does not throw or warn: it yields an empty list, and `useAiSurfaceEnabled`
* turns an empty list into "hide the entire AI surface". That is also the
* CORRECT behaviour for a seat-less user or a Community-Edition deployment with
* no `service-ai` — so a parse miss is indistinguishable from the legitimate
* hidden state, with no error, no 403 and no log to notice it by. Teaching the
* consumer first means the producer can convert on its own schedule instead of
* having to land in lockstep with this file.
* Why tolerance here rather than one shape and a parse error: an unrecognised
* shape does not throw or warn. It yields an empty list, and `useAiSurfaceEnabled`
* turns an empty list into "hide the entire AI surface". That is also the CORRECT
* behaviour for a seat-less user or a Community-Edition deployment with no
* `service-ai` — so a parse miss is indistinguishable from the legitimate hidden
* state, with no error, no 403 and no log to notice it by. Reading every shape
* this route has ever answered in is what kept the server conversions from having
* to land in lockstep with a console release.
*/
export function extractAgentList(payload: unknown): RawAgent[] {
const isEnvelope =
Expand Down
Loading