From fce8331c4de3f77afd9bd748a8869b18d68bdbf6 Mon Sep 17 00:00:00 2001 From: JakeSCahill Date: Tue, 27 Oct 2026 17:14:46 +0000 Subject: [PATCH] Look up config properties in the published reference, not the UI sample --- .github/workflows/validate-build.yml | 2 + package.json | 5 +- src/js/react/agentTools.js | 40 ++++-- .../fixture-published.json | 85 ++++++++++++ .../property-lookup-tool.test.js | 130 ++++++++++++++++++ 5 files changed, 245 insertions(+), 17 deletions(-) create mode 100644 tests/property-lookup-tool/fixture-published.json create mode 100644 tests/property-lookup-tool/property-lookup-tool.test.js diff --git a/.github/workflows/validate-build.yml b/.github/workflows/validate-build.yml index 6293337c..bf553746 100644 --- a/.github/workflows/validate-build.yml +++ b/.github/workflows/validate-build.yml @@ -99,6 +99,8 @@ jobs: run: npm run test:introduced-in-badge - name: Test the Ask AI latest-version tool run: npm run test:latest-version-tool + - name: Test the Ask AI config property lookup + run: npm run test:property-lookup-tool - name: Cache generated files uses: actions/cache@v4 with: diff --git a/package.json b/package.json index 878a0336..9778551b 100644 --- a/package.json +++ b/package.json @@ -97,13 +97,14 @@ "test:hero-scroll": "node --test tests/hero-scroll/*.test.js", "test:docs-page-weight": "node --test tests/docs-page-weight/*.test.js", "test:callout-preservation": "npx gulp generate:prism && node tests/callout-preservation/test-runner.js", - "test:all": "npm run test:playground && npm run test:interactive && npm run test:negative-cache && npm run test:head-meta && npm run test:latest-version-tool && npm run test:introduced-in-badge && npm run test:property-tooltips && npm run test:signin-nudge && npm run test:chat-panel-navigation && npm run test:toc-collapsible && npm run test:toc-active-anchor && npm run test:nav-scroll && npm run test:kapa-source-groups && npm run test:tooltip-touch && npm run test:tooltip-single-open && npm run test:lazy-askai && npm run test:nav-lazy-buckets && npm run test:has-code-blocks && npm run test:docs-page-weight && npm run test:anon-quota && npm run test:callout-preservation", + "test:all": "npm run test:playground && npm run test:interactive && npm run test:negative-cache && npm run test:head-meta && npm run test:latest-version-tool && npm run test:property-lookup-tool && npm run test:introduced-in-badge && npm run test:property-tooltips && npm run test:signin-nudge && npm run test:chat-panel-navigation && npm run test:toc-collapsible && npm run test:toc-active-anchor && npm run test:nav-scroll && npm run test:kapa-source-groups && npm run test:tooltip-touch && npm run test:tooltip-single-open && npm run test:lazy-askai && npm run test:nav-lazy-buckets && npm run test:has-code-blocks && npm run test:docs-page-weight && npm run test:anon-quota && npm run test:callout-preservation", "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 .", "test:toc-active-anchor": "node --test tests/toc-active-anchor/*.test.js", "test:introduced-in-badge": "node --test tests/introduced-in-badge/*.test.js", - "test:latest-version-tool": "node --test tests/latest-version-tool/*.test.js" + "test:latest-version-tool": "node --test tests/latest-version-tool/*.test.js", + "test:property-lookup-tool": "node --test tests/property-lookup-tool/*.test.js" }, "dependencies": { "@kapaai/agent-react": "^1.0.1", diff --git a/src/js/react/agentTools.js b/src/js/react/agentTools.js index 8db04779..1344fa12 100644 --- a/src/js/react/agentTools.js +++ b/src/js/react/agentTools.js @@ -362,24 +362,31 @@ const submitDocsFeedback = { }, } -// ——— Config property lookup (grounded in the shipped reference) ——————————— -// redpanda-properties.json ships in the UI bundle (the same file the property -// tooltips read). Cache the fetch so repeated lookups in a session are free. -let propertiesPromise = null -function loadProperties () { - if (propertiesPromise) return propertiesPromise - // redpanda-properties.json is a UI static_file, so (like blobl.wasm) Antora - // emits it to the SITE root in a normal build and to the UI root in the - // docs-ui preview. Mirror the WASM loader's resolution. - const rootPath = typeof window.uiRootPath !== 'undefined' ? window.uiRootPath : '/_' - const siteRoot = typeof window.siteRootPath !== 'undefined' ? window.siteRootPath : '' +// ——— Config property lookup (grounded in the published reference) ————————— +// Read the same property reference the property tooltips read: the JSON that +// head-meta names in , which the docs build +// publishes per release (691 properties). The redpanda-properties.json that +// ships in the UI bundle is a small sample for the docs-ui preview, which has +// no docs build behind it, so it is used there and nowhere else. Cache by URL +// so repeated lookups in a session are free. +const propertiesPromises = {} +function propertiesUrl () { + const meta = document.querySelector('meta[name="properties-json-url"]') + if (meta && meta.content) return meta.content const isPreview = typeof window.isUiPreview !== 'undefined' ? window.isUiPreview : false - const url = isPreview ? `${rootPath}/redpanda-properties.json` : `${siteRoot}/redpanda-properties.json` - propertiesPromise = fetch(url) + if (!isPreview) return null + const rootPath = typeof window.uiRootPath !== 'undefined' ? window.uiRootPath : '/_' + return `${rootPath}/redpanda-properties.json` +} +function loadProperties () { + const url = propertiesUrl() + if (!url) return Promise.reject(new Error('this page names no property reference')) + if (propertiesPromises[url]) return propertiesPromises[url] + propertiesPromises[url] = fetch(url) .then((r) => { if (!r.ok) throw new Error(`properties reference not found (${r.status})`); return r.json() }) .then((data) => data.properties || {}) - .catch((err) => { propertiesPromise = null; throw err }) // allow retry - return propertiesPromise + .catch((err) => { delete propertiesPromises[url]; throw err }) // allow retry + return propertiesPromises[url] } const lookupConfigProperty = { @@ -427,6 +434,9 @@ const lookupConfigProperty = { deprecated: rec.is_deprecated, minimum: rec.minimum, maximum: rec.maximum, + cloudEditable: rec.cloud_editable, + cloudReadonly: rec.cloud_readonly, + cloudByocOnly: rec.cloud_byoc_only, } }, } diff --git a/tests/property-lookup-tool/fixture-published.json b/tests/property-lookup-tool/fixture-published.json new file mode 100644 index 00000000..d34adf3f --- /dev/null +++ b/tests/property-lookup-tool/fixture-published.json @@ -0,0 +1,85 @@ +{ + "properties": { + "default_topic_replications": { + "category": "other", + "cloud_byoc_only": false, + "cloud_editable": false, + "cloud_readonly": true, + "cloud_supported": true, + "config_scope": "cluster", + "default": 1, + "defined_in": "src/v/config/configuration.cc", + "description": "Default replication factor for new topics.", + "example": "`1`", + "gets_restored": true, + "is_deprecated": false, + "is_enterprise": false, + "is_secret": false, + "minimum": 1, + "name": "default_topic_replications", + "needs_restart": false, + "nullable": false, + "type": "integer", + "visibility": "user", + "description_html": "Default replication factor for new topics." + }, + "write_caching_default": { + "category": "other", + "cloud_byoc_only": false, + "cloud_editable": false, + "cloud_readonly": false, + "cloud_supported": false, + "config_scope": "cluster", + "default": "false", + "defined_in": "src/v/config/configuration.cc", + "description": "The default write caching mode to apply to user topics. Write caching acknowledges a message as soon as it is received and acknowledged on a majority of brokers, without waiting for it to be written to disk. With `acks=all`, this provides lower latency while still ensuring that a majority of brokers acknowledge the write. \n\nFsyncs follow `raft_replica_max_pending_flush_bytes` and `raft_replica_max_flush_delay_ms`, whichever is reached first.\n\nThe `write_caching_default` cluster property can be overridden with the `write.caching` topic property, unless set to `disabled`, which takes precedence over topic overrides and disables write caching for the entire cluster.", + "enum": [ + "true", + "false", + "disabled" + ], + "example": "`true`", + "gets_restored": true, + "is_deprecated": false, + "is_enterprise": false, + "is_secret": false, + "links": { + "`raft_replica_max_flush_delay_ms`": "#raft_replica_max_flush_delay_ms", + "`raft_replica_max_pending_flush_bytes`": "#raft_replica_max_pending_flush_bytes" + }, + "name": "write_caching_default", + "needs_restart": false, + "nullable": false, + "see_also": [ + "xref:reference:properties/topic-properties.adoc#write-caching[`write.caching`]", + "xref:develop:manage-topics/config-topics.adoc#configure-write-caching[Write caching]" + ], + "type": "string", + "visibility": "user", + "description_html": "
\n

The default write caching mode to apply to user topics. Write caching acknowledges a message as soon as it is received and acknowledged on a majority of brokers, without waiting for it to be written to disk. With acks=all, this provides lower latency while still ensuring that a majority of brokers acknowledge the write.

\n
\n
\n

Fsyncs follow raft_replica_max_pending_flush_bytes and raft_replica_max_flush_delay_ms, whichever is reached first.

\n
\n
\n

The write_caching_default cluster property can be overridden with the write.caching topic property, unless set to disabled, which takes precedence over topic overrides and disables write caching for the entire cluster.

\n
" + }, + "log_segment_size": { + "category": "other", + "cloud_byoc_only": false, + "cloud_editable": false, + "cloud_readonly": false, + "cloud_supported": false, + "config_scope": "cluster", + "default": 134217728, + "defined_in": "src/v/config/configuration.cc", + "description": "Default log segment size in bytes for topics which do not set segment.bytes", + "example": "`2147483648`", + "gets_restored": true, + "is_deprecated": false, + "is_enterprise": false, + "is_secret": false, + "minimum": 1048576, + "name": "log_segment_size", + "needs_restart": false, + "nullable": false, + "type": "integer", + "visibility": "tunable", + "description_html": "Default log segment size in bytes for topics which do not set segment.bytes" + } + } +} \ No newline at end of file diff --git a/tests/property-lookup-tool/property-lookup-tool.test.js b/tests/property-lookup-tool/property-lookup-tool.test.js new file mode 100644 index 00000000..d9965be0 --- /dev/null +++ b/tests/property-lookup-tool/property-lookup-tool.test.js @@ -0,0 +1,130 @@ +'use strict' + +// Verifies the Ask AI lookup_config_property tool (src/js/react/agentTools.js) +// reads the published property reference that head-meta names in +// , the same file the property tooltips read, +// and uses the sample redpanda-properties.json from the UI bundle only in the +// docs-ui preview. +// +// fixture-published.json holds three records copied from the published +// redpanda-properties-v26.2.4.json. The sample in src/static has none of +// default_topic_replications or write_caching_default. +// +// agentTools.js is an ES module and CI runs Node 18, so it is loaded through +// vm with its single export rewritten into a global assignment (see +// tests/latest-version-tool for the same harness). + +const test = require('node:test') +const assert = require('node:assert/strict') +const fs = require('node:fs') +const path = require('node:path') +const vm = require('node:vm') + +const ROOT = path.join(__dirname, '..', '..') +const TOOLS = path.join(ROOT, 'src/js/react/agentTools.js') +const PUBLISHED = JSON.parse(fs.readFileSync(path.join(__dirname, 'fixture-published.json'), 'utf8')) +const SAMPLE = JSON.parse(fs.readFileSync(path.join(ROOT, 'src/static/redpanda-properties.json'), 'utf8')) +const META_URL = '/streaming/current/reference/_attachments/redpanda-properties-v26.2.4.json' + +function loadTool ({ metas = {}, preview = false, files = {} } = {}) { + const source = fs.readFileSync(TOOLS, 'utf8') + const rewritten = source.replace(/^export const agentTools =/m, 'globalThis.agentTools =') + assert.notEqual(rewritten, source, 'the agentTools export shape changed; update this harness') + const fetched = [] + const querySelector = (sel) => { + const m = sel.match(/^meta\[name="([^"]+)"\]$/) + if (m) return m[1] in metas ? { content: metas[m[1]] } : null + return null + } + const window = { + location: { origin: 'https://docs.redpanda.com', href: 'https://docs.redpanda.com/' }, + addEventListener: () => {}, + uiRootPath: '/_', + siteRootPath: '', + } + if (preview) window.isUiPreview = true + const context = { + console, + URL, + fetch: (url) => { + fetched.push(url) + if (!(url in files)) return Promise.resolve({ ok: false, status: 404 }) + return Promise.resolve({ ok: true, json: () => Promise.resolve(files[url]) }) + }, + window, + document: { + querySelector, + querySelectorAll: () => [], + addEventListener: () => {}, + body: { getAttribute: () => 'streaming' }, + }, + } + context.globalThis = context + vm.runInNewContext(rewritten, context) + return { tool: context.agentTools.find((t) => t.name === 'lookup_config_property'), fetched } +} + +test('reads the published reference named by the page meta', async () => { + const { tool, fetched } = loadTool({ + metas: { 'properties-json-url': META_URL }, + files: { [META_URL]: PUBLISHED, '/redpanda-properties.json': SAMPLE }, + }) + const r = await tool.execute({ property: 'default_topic_replications' }) + assert.equal(r.found, true) + assert.equal(r.default, 1) + assert.equal(r.scope, 'cluster') + assert.equal(r.cloudSupported, true) + assert.deepEqual(fetched, [META_URL]) +}) + +test('finds a property only the published reference has', async () => { + assert.equal('write_caching_default' in SAMPLE.properties, false) + const { tool } = loadTool({ metas: { 'properties-json-url': META_URL }, files: { [META_URL]: PUBLISHED } }) + const r = await tool.execute({ property: 'write_caching_default' }) + assert.equal(r.found, true) + assert.equal(r.name, 'write_caching_default') +}) + +test('returns the Cloud flags the published reference carries', async () => { + const { tool } = loadTool({ metas: { 'properties-json-url': META_URL }, files: { [META_URL]: PUBLISHED } }) + const r = await tool.execute({ property: 'default_topic_replications' }) + const rec = PUBLISHED.properties.default_topic_replications + assert.equal(r.cloudEditable, rec.cloud_editable) + assert.equal(r.cloudReadonly, rec.cloud_readonly) + assert.equal(r.cloudByocOnly, rec.cloud_byoc_only) +}) + +test('fetches the reference once for repeated lookups', async () => { + const { tool, fetched } = loadTool({ metas: { 'properties-json-url': META_URL }, files: { [META_URL]: PUBLISHED } }) + await tool.execute({ property: 'default_topic_replications' }) + await tool.execute({ property: 'log_segment_size' }) + assert.deepEqual(fetched, [META_URL]) +}) + +test('on a real page with no meta, says so and never reads the sample', async () => { + const { tool, fetched } = loadTool({ files: { '/redpanda-properties.json': SAMPLE } }) + const r = await tool.execute({ property: 'log_segment_size' }) + assert.equal(r.found, false) + assert.match(r.error, /names no property reference/) + assert.deepEqual(fetched, []) +}) + +test('in the docs-ui preview with no meta, reads the bundled sample', async () => { + const { tool, fetched } = loadTool({ preview: true, files: { '/_/redpanda-properties.json': SAMPLE } }) + const name = Object.keys(SAMPLE.properties)[0] + const r = await tool.execute({ property: name }) + assert.equal(r.found, true) + assert.deepEqual(fetched, ['/_/redpanda-properties.json']) +}) + +test('a failed fetch reports an error and is retried on the next lookup', async () => { + const files = {} + const { tool, fetched } = loadTool({ metas: { 'properties-json-url': META_URL }, files }) + const first = await tool.execute({ property: 'default_topic_replications' }) + assert.equal(first.found, false) + assert.match(first.error, /not found \(404\)/) + files[META_URL] = PUBLISHED + const second = await tool.execute({ property: 'default_topic_replications' }) + assert.equal(second.found, true) + assert.equal(fetched.length, 2) +})