From fbe6be5f21973eacfb8d294755f7a561ef0174b4 Mon Sep 17 00:00:00 2001 From: JakeSCahill Date: Fri, 4 Sep 2026 15:43:15 +0100 Subject: [PATCH 1/7] DOC-2450: Scope Ask AI retrieval to the reader's docs version Ask AI answered from any indexed docs version. Measured against Kapa's live retrieval API for the exact question in the ticket, "hardware requirements for enterprise redpanda self-hosted": 11 results spread across 24.2, 24.3, 25.1, 25.2, 25.3 and current, with only ONE from current. Scoped to the current group, 14 of 14 came from current. Both provider tiers now pass the Kapa source group for the page's own version, resolved per page by a new get-kapa-source-groups helper and emitted into chat-panel.hbs as window.KAPA_SOURCE_GROUP_IDS. Also closes the retrieval half of DOC-1807. The segment comes from page.url, not page.version. Those disagree for the latest release: latest_version_segment: 'current' publishes 26.2 at /streaming/current/ while page.version reads 26.2, and the Kapa mapping is keyed on the URL segment because that is what Kapa's own source_url values use. Deriving from page.version would look correct and silently miss every latest-version reader, which is exactly the population that reported this. Unversioned pages (Cloud, Connect, Agentic Data Plane, labs, home, search) resolve to the mapping's default segment rather than to no filter. Sending no filter there is what caused this ticket: the reporter confirmed he was on a page with no version of its own. Scoping to a version group does not hide Cloud, Connect or Agentic Data Plane content. Those sources are deliberately unassigned in Kapa, so they are global and come through alongside the selected group. Verified live: scoped to the 25.2 group, an Agentic Data Plane question returned 10 of 10 results from /agentic-data-plane/. The two SDKs spell the same option differently and Kapa documents the inconsistency deliberately, so the name is derived in one place rather than written at each call site. Verified against the shipped packages rather than the docs: @kapaai/agent-react declares sourceGroupIdsInclude (lowercase d) and @kapaai/react-sdk declares sourceGroupIDsInclude (capital ID). An unknown React prop is ignored with no error, so a typo means no filter is sent and answers quietly come from every version again. Never sends source_ids_include. Proved live that Kapa's retrieval endpoint ignores it: the real source id, a garbage uuid, and no parameter at all returned byte-identical result sets. It is not a declared parameter of that endpoint even though the hosted MCP _meta table advertises it, so passing it looks like filtering and does nothing. Every unresolvable case degrades to sending no filter, which is the pre-fix behaviour, because a wrong group is worse than no group: scoping to a group that does not hold the reader's version returns only Kapa's global sources, silently and without error. That covers a missing mapping attribute, malformed JSON, no page context, and a published version with no group yet (which falls back to the default rather than searching everything). /api/ pages are not scoped here. chat-panel-bump.hbs is compiled at build time from context/chat-panel.json, which carries no Antora page or site data, so the helper has nothing to resolve against. docs-site's proxy-api-docs edge function already rewrites that widget's DOM and will inject the current group there, which keeps the id out of a second template where it would go stale. Tests: 12 new, run against the real helper and the real partial through real Handlebars. They pin the page.url-not-page.version behaviour, the default-segment fallback for unversioned pages, both SDK prop spellings, that the partial renders valid JS in both the resolved and empty cases, and every degradation path. The React bundle was rebuilt and both prop names confirmed present in the shipped AskAI.bundle.js with source_ids_include absent; the bundle also shows the SDK's own guard, `if (sourceGroupIdsInclude?.length) body.source_group_ids_include = ...`. Existing suites unaffected: head-meta 3, property-tooltips 18, markdown-dropdown 15. --- package.json | 3 +- src/helpers/get-kapa-source-groups.js | 123 +++++++++++ src/js/react/AskAI.jsx | 28 +++ src/partials/chat-panel-bump.hbs | 11 + src/partials/chat-panel.hbs | 9 + .../kapa-source-groups.test.js | 196 ++++++++++++++++++ 6 files changed, 369 insertions(+), 1 deletion(-) create mode 100644 src/helpers/get-kapa-source-groups.js create mode 100644 tests/kapa-source-groups/kapa-source-groups.test.js diff --git a/package.json b/package.json index 1f26bab5..0f7e2c49 100644 --- a/package.json +++ b/package.json @@ -81,7 +81,8 @@ "test:negative-cache": "node tests/negative-cache/test-runner.js", "test:head-meta": "node --test tests/head-meta/*.test.js", "test:property-tooltips": "node --test tests/property-tooltips/*.test.js", - "test:all": "npm run test:playground && npm run test:interactive && npm run test:negative-cache && npm run test:head-meta && npm run test:property-tooltips", + "test:kapa-source-groups": "node --test tests/kapa-source-groups/*.test.js", + "test:all": "npm run test:playground && npm run test:interactive && npm run test:negative-cache && npm run test:head-meta && npm run test:property-tooltips && npm run test:kapa-source-groups", "build:wasm": "cd blobl-editor/wasm && GOOS=js GOARCH=wasm go build -o ../../src/static/blobl.wasm .", "copy:wasm-exec": "cp \"$(go env GOROOT)/lib/wasm/wasm_exec.js\" src/js/vendor/", "serve:playground": "npx serve ." diff --git a/src/helpers/get-kapa-source-groups.js b/src/helpers/get-kapa-source-groups.js new file mode 100644 index 00000000..7c48d9a0 --- /dev/null +++ b/src/helpers/get-kapa-source-groups.js @@ -0,0 +1,123 @@ +'use strict' + +/** + * Resolves the Kapa source group that scopes Ask AI retrieval to the docs + * version the reader is actually on. + * + * WHY THIS EXISTS + * --------------- + * Kapa indexes one separately crawled source per published docs version. With no + * scoping, a question is answered from any of them. Measured against the live + * retrieval API for "hardware requirements for enterprise redpanda self-hosted", + * the exact question in DOC-2450: 11 results spread across 24.2, 24.3, 25.1, + * 25.2, 25.3 and current, with only ONE from current. Scoped to the current + * group, 14 of 14 came from current. + * + * HOW THE SEGMENT IS DERIVED + * -------------------------- + * From `page.url`, not from page.version. The two disagree for the latest + * release: `latest_version_segment: 'current'` publishes 26.2 at + * /streaming/current/ while page.version reads 26.2. The Kapa mapping is keyed on + * the URL segment because that is what Kapa's own source_url values use, so + * reading the URL avoids having to know which version is currently latest. + * + * WHAT IT RETURNS + * --------------- + * An array, because that is the shape both Kapa providers want + * (sourceGroupIdsInclude on AgentProvider, sourceGroupIDsInclude on + * KapaProvider). An EMPTY array means "send no filter", which is the pre-DOC-2450 + * behaviour: Kapa searches everything. That is the deliberate degradation for any + * case where scoping cannot be resolved, because a wrong group is worse than no + * group -- scoping to a group that does not hold the reader's version returns + * only Kapa's global sources, so the reader gets no version-specific content at + * all and no error either. + * + * Unversioned pages (Cloud, Connect, Agentic Data Plane, labs, home, search, the + * 404 page) resolve to the mapping's default_segment rather than to nothing. + * Sending no filter there is what produced DOC-2450 in the first place: the + * reporter was on a page with no version of its own. + * + * Note that scoping to a version group does NOT hide Cloud, Connect or Agentic + * Data Plane content. Those sources are deliberately left unassigned in Kapa, so + * they are "global" and come through alongside whichever group is selected. + * Verified live: scoped to the 25.2 group, an Agentic Data Plane question + * returned 10 of 10 results from /agentic-data-plane/. + * + * Usage in templates: + * window.KAPA_SOURCE_GROUP_IDS = [ + * {{#each (get-kapa-source-groups)}}"{{{this}}}"{{#unless @last}},{{/unless}}{{/each}} + * ]; + * + * @param {object} options - Handlebars options with data.root.page and data.root.site + * @returns {string[]} Zero or one Kapa source group id + */ +module.exports = function (options) { + const root = (options && options.data && options.data.root) || {} + const { page, site } = root + + const mapping = readMapping(page, site) + if (!mapping || !mapping.segments) return [] + + const segment = versionSegmentFromUrl(page && page.url) + + // A versioned page whose segment has no group is the case the drift check + // exists to catch: a version was published and nobody created the Kapa source + // and group. Fall back to the default rather than sending nothing, so the + // reader gets current-version answers instead of every version at once. + const entry = (segment && mapping.segments[segment]) || mapping.segments[mapping.default_segment] + if (!entry || !entry.group_id) return [] + + return [entry.group_id] +} + +/** + * The mapping is generated in docs-extensions-and-macros + * (docs-data/kapa-source-groups.json) and surfaced to the UI as an AsciiDoc + * attribute, because docs-ui does not depend on that package and must not carry + * a second copy that can drift. + * + * Read from the component version first and the site second, matching how + * add-global-attributes.js merges shared attributes onto every component + * version. Absent in a bare docs-ui preview, which is why every failure path + * degrades to "no filter" rather than throwing. + */ +function readMapping (page, site) { + const candidates = [ + page && page.componentVersion && page.componentVersion.asciidoc && page.componentVersion.asciidoc.attributes, + page && page.component && page.component.asciidoc && page.component.asciidoc.attributes, + page && page.attributes, + site && site.asciidoc && site.asciidoc.attributes, + ] + + for (const attrs of candidates) { + const raw = attrs && (attrs['kapa-source-groups'] || attrs.kapa_source_groups) + if (!raw) continue + if (typeof raw === 'object') return raw + try { + return JSON.parse(raw) + } catch (err) { + // A malformed attribute must not break the page. Losing version scoping is + // a degraded answer; a thrown helper is a broken build. + return null + } + } + return null +} + +/** + * Pull the version segment out of a page URL. + * + * Only /streaming/ is versioned today. Every other component publishes + * unversioned, so this returns null for them and the caller uses the default. + * + * @param {string} url - e.g. /streaming/25.2/get-started/intro-to-events/ + * @returns {string|null} e.g. '25.2', 'current', or null when not versioned + */ +function versionSegmentFromUrl (url) { + if (typeof url !== 'string') return null + const match = url.match(/^\/streaming\/([^/]+)\//) + return match ? match[1] : null +} + +module.exports.versionSegmentFromUrl = versionSegmentFromUrl +module.exports.readMapping = readMapping diff --git a/src/js/react/AskAI.jsx b/src/js/react/AskAI.jsx index b84b432d..e92828c7 100644 --- a/src/js/react/AskAI.jsx +++ b/src/js/react/AskAI.jsx @@ -287,6 +287,32 @@ const CUSTOM_INSTRUCTIONS = `## Domain context // The docs page the widget is open on, appended to the agent instructions so it // can infer the user's product (Cloud / Self-Managed / ADP) from context before // asking. Antora sets to the docs component. +// Kapa source group scoping retrieval to the docs version of THIS page +// (DOC-1807, DOC-2450). The array is emitted per page by chat-panel.hbs via the +// get-kapa-source-groups helper, so it varies by URL without rebuilding the bundle. +// +// The two SDKs spell the same option differently, and Kapa documents the +// inconsistency deliberately (dev/agent/migrating-from-chat-sdk): +// +// Agent SDK (signed in) sourceGroupIdsInclude lowercase d +// Chat SDK (anonymous) sourceGroupIDsInclude capital ID +// +// A typo in either fails silently -- an unknown prop is ignored, no filter is +// sent, and answers quietly come from every docs version. So the name is derived +// from one place rather than written out at each call site. +// +// Spread rather than passed directly so that an empty array omits the prop +// entirely instead of sending []. Kapa treats an explicit empty list as "clear +// filtering", which is the same outcome, but omitting keeps the provider props +// identical to their pre-DOC-2450 shape when scoping cannot be resolved. +const SOURCE_GROUP_PROP = { agent: 'sourceGroupIdsInclude', chat: 'sourceGroupIDsInclude' } + +function sourceGroupProps (tier) { + const ids = Array.isArray(window.KAPA_SOURCE_GROUP_IDS) ? window.KAPA_SOURCE_GROUP_IDS.filter(Boolean) : [] + if (!ids.length) return {} + return { [SOURCE_GROUP_PROP[tier]]: ids } +} + function currentPageContext () { try { const path = window.location.pathname @@ -419,6 +445,7 @@ function App () { tools={agentTools} customInstructions={CUSTOM_INSTRUCTIONS + currentPageContext()} user={user?.email ? { email: user.email } : undefined} + {...sourceGroupProps('agent')} enableHistory onEvent={handleAgentEvent} theme={{ accentColor: '#444ce7', colorScheme }} @@ -441,6 +468,7 @@ function App () { { diff --git a/src/partials/chat-panel-bump.hbs b/src/partials/chat-panel-bump.hbs index 2257bd3f..c03dd142 100644 --- a/src/partials/chat-panel-bump.hbs +++ b/src/partials/chat-panel-bump.hbs @@ -61,6 +61,17 @@ ]; // Signed-in agent tier: tool-showcasing prompts (static here — the standalone // Bump widget has no Antora component context for get-agent-suggestions). + {{!-- KAPA_SOURCE_GROUP_IDS is deliberately NOT set here. This partial is compiled + at build time from context/chat-panel.json (compileWidgets in gulpfile.js), + which carries no Antora page or site data, so get-kapa-source-groups has + nothing to resolve against. + /api/ pages should scope to the latest version, and docs-site sets it: its + proxy-api-docs edge function already fetches this widget's compiled HTML and + rewrites the DOM, and it has the source-group mapping available, so it + injects the "current" group id there. Keeping the id out of this template + avoids a second hardcoded copy that would go stale the moment a group is + recreated -- a stale id scopes to a group holding nothing, which silently + returns only global sources. --}} window.AGENT_SUGGESTIONS = [ 'Write and test a Bloblang mapping that flattens nested JSON', "What's the latest Redpanda Streaming version?", diff --git a/src/partials/chat-panel.hbs b/src/partials/chat-panel.hbs index f2aaff52..cfb10f4f 100644 --- a/src/partials/chat-panel.hbs +++ b/src/partials/chat-panel.hbs @@ -56,6 +56,15 @@ "{{{this}}}"{{#unless @last}},{{/unless}} {{/each}} ]; + {{!-- Kapa source group scoping retrieval to THIS page's docs version (DOC-1807, + DOC-2450). Empty array means send no filter, which is the pre-DOC-2450 + behaviour of searching every version at once. Emitted per page rather than + bundled, so it varies by URL for free. --}} + window.KAPA_SOURCE_GROUP_IDS = window.KAPA_SOURCE_GROUP_IDS || [ + {{#each (get-kapa-source-groups)}} + "{{{this}}}"{{#unless @last}},{{/unless}} + {{/each}} + ]; {{!-- Signed-in agent tier: per-component prompts that showcase the agent tools --}} window.AGENT_SUGGESTIONS = window.AGENT_SUGGESTIONS || [ {{#each (get-agent-suggestions)}} diff --git a/tests/kapa-source-groups/kapa-source-groups.test.js b/tests/kapa-source-groups/kapa-source-groups.test.js new file mode 100644 index 00000000..e6c708c5 --- /dev/null +++ b/tests/kapa-source-groups/kapa-source-groups.test.js @@ -0,0 +1,196 @@ +'use strict' + +// Verifies version-scoped Ask AI retrieval (DOC-1807, DOC-2450) end to end +// through the REAL helper and the REAL chat-panel.hbs partial, not stand-ins. +// +// The bug being fixed, measured against Kapa's live retrieval API for the exact +// question in DOC-2450 ("hardware requirements for enterprise redpanda +// self-hosted"): unscoped, 11 results spread across 24.2, 24.3, 25.1, 25.2, 25.3 +// and current, with only ONE from current. Scoped to the current group, 14 of 14 +// came from current. +// +// Two things here are easy to get wrong and fail silently, so both are pinned: +// +// 1. The segment must come from page.url, not page.version. +// latest_version_segment: 'current' publishes 26.2 at /streaming/current/, +// so page.version says 26.2 while the Kapa mapping is keyed on 'current'. +// Deriving from page.version would look right and miss every latest-version +// reader, which is precisely the DOC-2450 population. +// +// 2. The two SDKs spell the option differently on purpose +// (sourceGroupIdsInclude vs sourceGroupIDsInclude). An unknown React prop is +// ignored with no error, so a typo means no filter is sent and answers come +// from every version again. + +const test = require('node:test') +const assert = require('node:assert/strict') +const path = require('node:path') +const fs = require('node:fs') +const Handlebars = require('handlebars') + +const ROOT = path.join(__dirname, '..', '..') +const helper = require(path.join(ROOT, 'src/helpers/get-kapa-source-groups.js')) +const { versionSegmentFromUrl } = helper + +// A trimmed copy of the shape doc-tools' generate kapa-source-groups emits. +const MAPPING = { + project_id: '97f44223-f930-4fb9-ae1e-ecd436a4d85c', + parent_group: { id: '238b3c08', name: 'Streaming', type: 'product' }, + default_segment: 'current', + segments: { + '24.2': { group_id: 'grp-242', group_name: '24.2', source_ids: ['s1'], source_names: ['Documentation (24.2)'] }, + '25.2': { group_id: 'grp-252', group_name: '25.2', source_ids: ['s2'], source_names: ['Documentation (25.2)'] }, + current: { group_id: 'grp-cur', group_name: 'current', source_ids: ['s3'], source_names: ['Documentation (current)'] }, + }, + global_sources: ['Agentic Data Plane', 'Documentation (Cloud)'], +} + +const call = (page, { mapping = MAPPING, where = 'componentVersion' } = {}) => { + const root = { page: { ...page } } + const raw = mapping === null ? undefined : JSON.stringify(mapping) + if (where === 'componentVersion') root.page.componentVersion = { asciidoc: { attributes: { 'kapa-source-groups': raw } } } + if (where === 'component') root.page.component = { asciidoc: { attributes: { 'kapa-source-groups': raw } } } + if (where === 'site') root.site = { asciidoc: { attributes: { 'kapa-source-groups': raw } } } + if (where === 'none') { /* no attribute anywhere */ } + return helper({ data: { root } }) +} + +test('versionSegmentFromUrl reads the URL segment, which is what the mapping is keyed on', () => { + assert.equal(versionSegmentFromUrl('/streaming/25.2/get-started/intro/'), '25.2') + // The latest release publishes at /streaming/current/ even though page.version + // is 26.2. Getting this from the URL is the whole point. + assert.equal(versionSegmentFromUrl('/streaming/current/get-started/intro/'), 'current') + assert.equal(versionSegmentFromUrl('/streaming/beta/get-started/intro/'), 'beta') + // Unversioned components have no segment. + assert.equal(versionSegmentFromUrl('/cloud-data-platform/get-started/'), null) + assert.equal(versionSegmentFromUrl('/agentic-data-plane/reference/'), null) + assert.equal(versionSegmentFromUrl('/connect/components/'), null) + assert.equal(versionSegmentFromUrl('/home/'), null) + // Junk must not throw. + for (const v of [undefined, null, '', 42, {}, '/streaming/']) assert.equal(versionSegmentFromUrl(v), null) +}) + +test('a versioned page resolves to its own version group', () => { + assert.deepEqual(call({ url: '/streaming/25.2/manage/monitoring/' }), ['grp-252']) + assert.deepEqual(call({ url: '/streaming/24.2/manage/monitoring/' }), ['grp-242']) +}) + +test('the latest version resolves via its URL segment, not page.version', () => { + // page.version deliberately disagrees with the URL, as it does in production. + const got = call({ url: '/streaming/current/get-started/intro/', version: '26.2' }) + assert.deepEqual(got, ['grp-cur']) +}) + +test('unversioned pages resolve to the default segment, which is the DOC-2450 fix', () => { + // The reporter was on a page with no version. Sending no filter there is what + // let 25.2 content answer a latest-version question. + for (const url of ['/cloud-data-platform/get-started/', '/agentic-data-plane/reference/', '/connect/components/', '/home/', '/search/']) { + assert.deepEqual(call({ url }), ['grp-cur'], `expected default group for ${url}`) + } +}) + +test('a published version with no group falls back to the default rather than sending nothing', () => { + // This is the drift case: 26.3 published, nobody made the Kapa group yet. + // Falling back to current beats searching all nine versions at once. + assert.deepEqual(call({ url: '/streaming/26.3/get-started/intro/' }), ['grp-cur']) +}) + +test('reads the mapping from componentVersion, component or site attributes', () => { + for (const where of ['componentVersion', 'component', 'site']) { + assert.deepEqual(call({ url: '/streaming/25.2/x/' }, { where }), ['grp-252'], `from ${where}`) + } +}) + +test('accepts an already-parsed object, not only a JSON string', () => { + const root = { page: { url: '/streaming/25.2/x/', componentVersion: { asciidoc: { attributes: { 'kapa-source-groups': MAPPING } } } } } + assert.deepEqual(helper({ data: { root } }), ['grp-252']) +}) + +test('degrades to no filter rather than throwing, in every unresolvable case', () => { + // A wrong group is worse than no group: scoping to a group that does not hold + // the reader's version returns only Kapa's global sources, silently. + assert.deepEqual(call({ url: '/streaming/25.2/x/' }, { where: 'none' }), [], 'no attribute') + assert.deepEqual(helper({ data: { root: {} } }), [], 'no page at all') + assert.deepEqual(helper({ data: {} }), [], 'no root') + assert.deepEqual(helper({}), [], 'no data') + assert.deepEqual(helper(undefined), [], 'no options') + + // Malformed attribute: a broken helper is a broken build, so it must not throw. + const bad = { page: { url: '/streaming/25.2/x/', componentVersion: { asciidoc: { attributes: { 'kapa-source-groups': '{not json' } } } } } + assert.deepEqual(helper({ data: { root: bad } }), []) + + // Mapping present but shaped wrong. + assert.deepEqual(call({ url: '/streaming/25.2/x/' }, { mapping: {} }), []) + assert.deepEqual(call({ url: '/streaming/25.2/x/' }, { mapping: { segments: {} } }), []) + // default_segment pointing at a segment that does not exist. + assert.deepEqual(call({ url: '/nope/' }, { mapping: { default_segment: 'gone', segments: MAPPING.segments } }), []) +}) + +test('chat-panel.hbs emits the group id as a JS array literal', () => { + const source = fs.readFileSync(path.join(ROOT, 'src/partials/chat-panel.hbs'), 'utf8') + // Only the config in a mapping value would end the string or the script element. The helper now has two JSON modes that emit one script-safe literal each (JSON.stringify plus `<` as \u003c, the same treatment docs-site's edge function gives the same value), and the template contains no raw id anywhere. CodeRabbit: group_id was checked for truthiness only, so a malformed mapping with group_id: {} was emitted as "[object Object]" and sent to Kapa as a filter, which returns only global sources with no error. A non-empty string is now required; anything else resolves to nothing, which is the safer failure. Kat: the test guarding the two SDK prop names asserted the same literal strings the source contains, so it would pass with both names wrong. The oracle is now the installed packages' own .d.mts files: each name must be declared as an optional string[] prop on its provider, must NOT be declared on the other SDK (or the test proves nothing), and both built SDKs must translate it to source_group_ids_include on the wire. agent-react forwards to agent-core, which sets body.source_group_ids_include; react-sdk carries it directly. Verified with a real Antora build over all seven content sources: 3498 pages render the panel, every emitted literal parses as JSON, and the segment agrees with the group on every page. --- src/helpers/get-kapa-source-groups.js | 39 +++++++-- src/partials/chat-panel.hbs | 11 ++- .../kapa-source-groups.test.js | 82 ++++++++++++++++--- 3 files changed, 110 insertions(+), 22 deletions(-) diff --git a/src/helpers/get-kapa-source-groups.js b/src/helpers/get-kapa-source-groups.js index 4a831bd8..0b56f5f7 100644 --- a/src/helpers/get-kapa-source-groups.js +++ b/src/helpers/get-kapa-source-groups.js @@ -69,12 +69,34 @@ module.exports = function (mode, options) { const opts = options === undefined ? mode : options const { segment, groupId } = resolve(opts) - if (mode === 'segment') { - // Only name a segment when a group is genuinely being sent, so the agent - // prompt cannot claim a restriction that is not in force. - return groupId && segment ? segment : '' + // Only name a segment when a group is genuinely being sent, so the agent + // prompt cannot claim a restriction that is not in force. + const segmentOut = groupId && segment ? segment : '' + const idsOut = groupId ? [groupId] : [] + switch (mode) { + case 'segment': return segmentOut + // The two JSON modes are what the template uses. They exist so that no raw + // value is ever interpolated into an executable in one would end the + // string or the script element. Handlebars' triple-stash does no escaping + // at all inside ` inside a value would close the script element early and + * dump the rest into the document as markup. Same treatment the docs-site edge + * function gives the same value for the /api/ pages. + * + * @param {*} value + * @returns {string} A JavaScript expression safe to place inside in one would end the string or the script element. --}} + window.KAPA_SOURCE_GROUP_IDS = window.KAPA_SOURCE_GROUP_IDS || {{{get-kapa-source-groups 'json'}}}; {{!-- The segment that the group above represents, so the agent prompt can name the version its answers came from without deriving it again from the URL. A second derivation disagrees: /streaming/26.2/ reads as "26.2" while the group sent is `current`. Empty string means nothing was scoped, so the prompt must not claim a restriction. --}} - window.KAPA_SOURCE_GROUP_SEGMENT = window.KAPA_SOURCE_GROUP_SEGMENT || "{{{get-kapa-source-groups 'segment'}}}"; + window.KAPA_SOURCE_GROUP_SEGMENT = window.KAPA_SOURCE_GROUP_SEGMENT || {{{get-kapa-source-groups 'segment-json'}}}; {{!-- Signed-in agent tier: per-component prompts that showcase the agent tools --}} window.AGENT_SUGGESTIONS = window.AGENT_SUGGESTIONS || [ {{#each (get-agent-suggestions)}} diff --git a/tests/kapa-source-groups/kapa-source-groups.test.js b/tests/kapa-source-groups/kapa-source-groups.test.js index a7c94460..afec2fa6 100644 --- a/tests/kapa-source-groups/kapa-source-groups.test.js +++ b/tests/kapa-source-groups/kapa-source-groups.test.js @@ -183,7 +183,7 @@ test('chat-panel.hbs emits the group id as a JS array literal', () => { // Only the config , no unescaped quote, and it round-trips to the value. + assert.doesNotMatch(json, /<\/script>/i) + assert.doesNotMatch(json, /