Skip to content
Open
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
2 changes: 2 additions & 0 deletions .github/workflows/validate-build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
40 changes: 25 additions & 15 deletions src/js/react/agentTools.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 <meta name="properties-json-url">, 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 = {
Expand Down Expand Up @@ -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,
}
},
}
Expand Down
85 changes: 85 additions & 0 deletions tests/property-lookup-tool/fixture-published.json
Original file line number Diff line number Diff line change
@@ -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": "<div class=\"paragraph\">\n<p>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 <code>acks=all</code>, this provides lower latency while still ensuring that a majority of brokers acknowledge the write.</p>\n</div>\n<div class=\"paragraph\">\n<p>Fsyncs follow <code>raft_replica_max_pending_flush_bytes</code> and <code>raft_replica_max_flush_delay_ms</code>, whichever is reached first.</p>\n</div>\n<div class=\"paragraph\">\n<p>The <code>write_caching_default</code> cluster property can be overridden with the <code>write.caching</code> topic property, unless set to <code>disabled</code>, which takes precedence over topic overrides and disables write caching for the entire cluster.</p>\n</div>"
},
"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"
}
}
}
130 changes: 130 additions & 0 deletions tests/property-lookup-tool/property-lookup-tool.test.js
Original file line number Diff line number Diff line change
@@ -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
// <meta name="properties-json-url">, 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)
})
Loading