From 8b65fcbdb13fabd7ae59f62300d1a9f55f34fdb3 Mon Sep 17 00:00:00 2001 From: Sisyphus Date: Sun, 3 May 2026 19:27:36 +0000 Subject: [PATCH] fix(authority): track supported job types Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus --- runtime/authority-capabilities.js | 80 ++++++++++++++++++ tests/authority-capabilities.mjs | 130 ++++++++++++++++++++++++++++++ 2 files changed, 210 insertions(+) diff --git a/runtime/authority-capabilities.js b/runtime/authority-capabilities.js index bd4e23b..e9cc30c 100644 --- a/runtime/authority-capabilities.js +++ b/runtime/authority-capabilities.js @@ -33,6 +33,10 @@ function normalizeFeatureName(value) { return String(value ?? "").trim().toLowerCase(); } +function normalizeJobType(value) { + return String(value ?? "").trim().toLowerCase(); +} + function addFeature(features, value) { const normalized = normalizeFeatureName(value); if (normalized) features.add(normalized); @@ -319,6 +323,71 @@ export function collectAuthorityFeatures(payload = {}) { return features; } +function collectJobTypesFromArray(jobTypes, value) { + if (!Array.isArray(value)) return false; + for (const item of value) { + const normalized = normalizeJobType(item); + if (normalized) jobTypes.add(normalized); + } + return true; +} + +function collectJobTypesFromEntries(jobTypes, value) { + if (!Array.isArray(value)) return false; + for (const entry of value) { + const normalized = normalizeJobType(entry?.type); + if (normalized) jobTypes.add(normalized); + } + return true; +} + +function collectSupportedJobTypes(payload = {}) { + const source = payload && typeof payload === "object" && !Array.isArray(payload) ? payload : {}; + const jobTypes = new Set(); + let known = source.supportedJobTypesKnown === true; + + const topLevelSupportedJobTypes = source.supportedJobTypes; + if (Array.isArray(topLevelSupportedJobTypes)) { + collectJobTypesFromArray(jobTypes, topLevelSupportedJobTypes); + known = + known || + topLevelSupportedJobTypes.length > 0 || + source.reason === "ok" || + Number(source.lastProbeAt || 0) > 0 || + source.installed === true || + source.healthy === true; + } + + for (const value of [ + source.jobs?.supportedTypes, + source.jobs?.builtinTypes, + source.jobs?.registry?.jobTypes, + source.features?.jobs?.supportedTypes, + source.features?.jobs?.builtinTypes, + source.features?.jobs?.registry?.jobTypes, + source.featureDetails?.jobs?.supportedTypes, + source.featureDetails?.jobs?.builtinTypes, + source.featureDetails?.jobs?.registry?.jobTypes, + source.core?.health?.jobRegistrySummary?.jobTypes, + ]) { + known = collectJobTypesFromArray(jobTypes, value) || known; + } + + for (const value of [ + source.jobs?.registry?.entries, + source.features?.jobs?.registry?.entries, + source.featureDetails?.jobs?.registry?.entries, + source.core?.health?.jobRegistrySummary?.entries, + ]) { + known = collectJobTypesFromEntries(jobTypes, value) || known; + } + + return { + supportedJobTypes: Array.from(jobTypes).sort(), + supportedJobTypesKnown: known, + }; +} + export function createDefaultAuthorityCapabilityState(overrides = {}) { return { enabledMode: "auto", @@ -332,6 +401,8 @@ export function createDefaultAuthorityCapabilityState(overrides = {}) { storagePrimaryReady: false, triviumPrimaryReady: false, jobsReady: false, + supportedJobTypes: [], + supportedJobTypesKnown: false, blobReady: false, features: [], missingFeatures: ["sql.query", "sql.mutation", "trivium.search", "jobs", "blob-or-private-files"], @@ -351,6 +422,7 @@ export function normalizeAuthorityCapabilityState(input = {}, settings = {}) { const source = input && typeof input === "object" && !Array.isArray(input) ? input : {}; const features = new Set((Array.isArray(source.features) ? source.features : []).map(normalizeFeatureName).filter(Boolean)); const readiness = createFeatureReadiness(features); + const supportedJobs = collectSupportedJobTypes(source); const missingFeatures = Array.isArray(source.missingFeatures) && source.missingFeatures.length ? source.missingFeatures.map(String) : collectMissingFeatures(readiness); @@ -380,6 +452,8 @@ export function normalizeAuthorityCapabilityState(input = {}, settings = {}) { storagePrimaryReady, triviumPrimaryReady, jobsReady, + supportedJobTypes: supportedJobs.supportedJobTypes, + supportedJobTypesKnown: supportedJobs.supportedJobTypesKnown, blobReady, features: Array.from(features).sort(), missingFeatures, @@ -396,6 +470,7 @@ export function normalizeAuthorityCapabilityState(input = {}, settings = {}) { export function normalizeAuthorityProbeResponse(payload = {}, context = {}) { const settings = normalizeAuthoritySettings(context.settings || {}); const features = collectAuthorityFeatures(payload); + const supportedJobs = collectSupportedJobTypes(payload); const readiness = createFeatureReadiness(features); const missingFeatures = collectMissingFeatures(readiness); const sessionReady = payload?.sessionReady ?? payload?.session?.ready ?? payload?.session?.active ?? true; @@ -408,6 +483,8 @@ export function normalizeAuthorityProbeResponse(payload = {}, context = {}) { sessionReady: Boolean(sessionReady), permissionReady: Boolean(permissionReady), features: Array.from(features), + supportedJobTypes: supportedJobs.supportedJobTypes, + supportedJobTypesKnown: supportedJobs.supportedJobTypesKnown, missingFeatures, reason: missingFeatures.length ? "missing-required-features" : "ok", endpoint: context.endpoint || "", @@ -519,6 +596,7 @@ export async function probeAuthorityCapabilities(options = {}) { payload = {}; } const features = collectAuthorityFeatures(payload); + const supportedJobs = collectSupportedJobTypes(payload); const readiness = createFeatureReadiness(features); const missingFeatures = collectMissingFeatures(readiness); const healthy = payload?.healthy ?? payload?.ok ?? true; @@ -544,6 +622,8 @@ export async function probeAuthorityCapabilities(options = {}) { sessionReady: Boolean(sessionReady), permissionReady: Boolean(permissionReady), features: Array.from(features), + supportedJobTypes: supportedJobs.supportedJobTypes, + supportedJobTypesKnown: supportedJobs.supportedJobTypesKnown, missingFeatures, reason, lastError: dataPlaneLastError, diff --git a/tests/authority-capabilities.mjs b/tests/authority-capabilities.mjs index 5c22e52..376872e 100644 --- a/tests/authority-capabilities.mjs +++ b/tests/authority-capabilities.mjs @@ -3,7 +3,9 @@ import assert from "node:assert/strict"; import { buildAuthorityProbeUrls, collectAuthorityFeatures, + createDefaultAuthorityCapabilityState, normalizeAuthorityCapabilityState, + normalizeAuthorityProbeResponse, normalizeAuthoritySettings, probeAuthorityCapabilities, } from "../runtime/authority-capabilities.js"; @@ -174,4 +176,132 @@ const relativeUnavailable = await probeAuthorityCapabilities({ assert.equal(relativeUnavailable.reason, "relative-url-unavailable"); assert.equal(relativeUnavailable.serverPrimaryReady, false); +// Regression: Authority capability normalization records explicit supported job types from probe payloads. +// When a probe payload provides jobs.supportedTypes, normalizeAuthorityCapabilityState should surface +// them as supportedJobTypes and set supportedJobTypesKnown = true. +const explicitJobTypesState = normalizeAuthorityCapabilityState( + { + installed: true, + healthy: true, + features: ["sql", "trivium", "jobs", "transfers.fs"], + jobs: { supportedTypes: ["authority.vector.rebuild", "authority.cache.invalidate"] }, + }, + defaultSettings, +); +assert.equal(explicitJobTypesState.supportedJobTypesKnown, true); +assert.deepEqual(explicitJobTypesState.supportedJobTypes, [ + "authority.cache.invalidate", + "authority.vector.rebuild", +]); + +// Regression: Current Authority builtin job list is known and excludes authority.vector.rebuild. +// When the probe payload reports a restricted job type list that omits authority.vector.rebuild, +// supportedJobTypes should NOT contain it and supportedJobTypesKnown should be true. +const restrictedJobTypesState = normalizeAuthorityCapabilityState( + { + installed: true, + healthy: true, + features: ["sql", "trivium", "jobs", "transfers.fs"], + jobs: { + builtinTypes: ["delay", "sql.backup", "trivium.flush", "fs.import-jsonl"], + registry: { + jobTypes: ["delay", "sql.backup", "trivium.flush", "fs.import-jsonl"], + entries: [{ type: "delay" }], + }, + }, + }, + defaultSettings, +); +assert.equal(restrictedJobTypesState.supportedJobTypesKnown, true); +assert.equal( + restrictedJobTypesState.supportedJobTypes.includes("authority.vector.rebuild"), + false, +); + +const authorityProbeJobTypesState = normalizeAuthorityProbeResponse( + { + healthy: true, + features: { + sql: { queryPage: true }, + trivium: { upsert: true }, + jobs: { + background: true, + builtinTypes: ["delay", "sql.backup", "trivium.flush", "fs.import-jsonl"], + }, + transfers: { fs: true }, + }, + jobs: { + builtinTypes: ["delay", "sql.backup", "trivium.flush", "fs.import-jsonl"], + registry: { + jobTypes: ["delay", "sql.backup", "trivium.flush", "fs.import-jsonl"], + entries: [{ type: "fs.import-jsonl" }], + }, + }, + core: { + health: { + jobRegistrySummary: { + jobTypes: ["delay", "sql.backup", "trivium.flush", "fs.import-jsonl"], + entries: [{ type: "trivium.flush" }], + }, + }, + }, + }, + { settings: defaultSettings, nowMs: 4000 }, +); +assert.equal(authorityProbeJobTypesState.supportedJobTypesKnown, true); +assert.deepEqual(authorityProbeJobTypesState.supportedJobTypes, [ + "delay", + "fs.import-jsonl", + "sql.backup", + "trivium.flush", +]); +assert.equal( + authorityProbeJobTypesState.supportedJobTypes.includes("authority.vector.rebuild"), + false, +); + +const renormalizedJobTypesState = normalizeAuthorityCapabilityState( + authorityProbeJobTypesState, + defaultSettings, +); +assert.equal(renormalizedJobTypesState.supportedJobTypesKnown, true); +assert.deepEqual(renormalizedJobTypesState.supportedJobTypes, authorityProbeJobTypesState.supportedJobTypes); + +const emptySupportedJobTypesProbeState = normalizeAuthorityProbeResponse( + { + healthy: true, + features: { + sql: { queryPage: true }, + trivium: { upsert: true }, + jobs: { background: true }, + transfers: { fs: true }, + }, + supportedJobTypes: [], + }, + { settings: defaultSettings, nowMs: 5000 }, +); +assert.equal(emptySupportedJobTypesProbeState.supportedJobTypesKnown, true); +assert.deepEqual(emptySupportedJobTypesProbeState.supportedJobTypes, []); + +// Regression: Legacy probes with generic jobs.background but no explicit job type list keep +// jobsReady === true and mark job type support as unknown (supportedJobTypesKnown === false). +const legacyProbeState = normalizeAuthorityCapabilityState( + { + installed: true, + healthy: true, + features: ["sql", "trivium", "jobs", "transfers.fs"], + }, + defaultSettings, +); +assert.equal(legacyProbeState.jobsReady, true); +assert.equal(legacyProbeState.supportedJobTypesKnown, false); +assert.deepEqual(legacyProbeState.supportedJobTypes, []); + +const defaultCapabilityState = normalizeAuthorityCapabilityState( + createDefaultAuthorityCapabilityState(), + defaultSettings, +); +assert.equal(defaultCapabilityState.supportedJobTypesKnown, false); +assert.deepEqual(defaultCapabilityState.supportedJobTypes, []); + console.log("authority-capabilities tests passed");