From b2884f7684d72cd0ad9269f89a146e1155f8cb06 Mon Sep 17 00:00:00 2001 From: JSONbored <49853598+JSONbored@users.noreply.github.com> Date: Sat, 11 Jul 2026 02:27:28 -0700 Subject: [PATCH 1/3] feat(review): make gittensor subnet integration an opt-in experimental plugin Add an experimental: manifest block (parallel to features:) so a self-host instance only fetches/tracks the gittensor subnet registry when a repo explicitly opts in via experimental.gittensor + GITTENSORY_EXPERIMENTAL_GITTENSOR. refresh-registry is skipped entirely on self-host until then, so a plain instance makes zero outbound contact with entrius/gittensor by default. Cloud is unaffected. Part of the isRegistered/isInstalled untangling epic (#5016). --- .gittensory.yml.example | 12 ++ config/examples/gittensory.full.yml | 12 ++ .../gittensory-engine/src/focus-manifest.ts | 58 ++++++++++ packages/gittensory-engine/src/index.ts | 4 + src/env.d.ts | 8 ++ src/index.ts | 15 ++- src/review/gittensor-wire.ts | 59 ++++++++++ src/signals/focus-manifest-loader.ts | 3 +- src/signals/focus-manifest.ts | 4 + test/helpers/d1.ts | 4 + test/unit/focus-manifest.test.ts | 22 ++++ test/unit/gittensor-wire.test.ts | 107 ++++++++++++++++++ test/unit/index.test.ts | 31 ++++- worker-configuration.d.ts | 4 +- wrangler.jsonc | 8 ++ 15 files changed, 345 insertions(+), 6 deletions(-) create mode 100644 src/review/gittensor-wire.ts create mode 100644 test/unit/gittensor-wire.test.ts diff --git a/.gittensory.yml.example b/.gittensory.yml.example index 2cb246b6dc..68b8d192f1 100644 --- a/.gittensory.yml.example +++ b/.gittensory.yml.example @@ -1123,6 +1123,18 @@ settings: # screenshots: false # improvementSignal: false +# Optional ecosystem/network PLUGINS -- distinct from `features:` above, which only toggles gittensory's own +# converged review capabilities. Each key here couples this instance to an external system and is OFF unless +# BOTH a deployment-wide GITTENSORY_EXPERIMENTAL_* env kill-switch AND an explicit per-repo `true` are set (no +# GITTENSORY_REVIEW_REPOS allowlist fallback -- unlike `features:`, there is no default-on path). `gittensor` +# is the first plugin: gittensory's original subnet mining-registry/scoring integration (per-repo emission +# share, maintainer cut, label multipliers pulled from the gittensor subnet's registry), now opt-in rather +# than a core dependency -- a self-host instance that never sets this has zero footprint from it: no fetch +# from the gittensor subnet registry, no local tracking/backfill of other repos on that subnet. Future +# plugins land in this same block as gittensory broadens beyond gittensor. +# experimental: +# gittensor: true + # Registry-review lane (#2435): lets a self-hosted maintainer point gittensory at their OWN structured # registry (e.g. a subnet/plugin/package catalog) without a gittensory code change -- reviewing additions # to a data file the same way it reviews code. Uncomment and set at least entryFileGlob + collectionField diff --git a/config/examples/gittensory.full.yml b/config/examples/gittensory.full.yml index 53ac17db39..3da33740a3 100644 --- a/config/examples/gittensory.full.yml +++ b/config/examples/gittensory.full.yml @@ -1136,6 +1136,18 @@ settings: # screenshots: false # improvementSignal: false +# Optional ecosystem/network PLUGINS -- distinct from `features:` above, which only toggles gittensory's own +# converged review capabilities. Each key here couples this instance to an external system and is OFF unless +# BOTH a deployment-wide GITTENSORY_EXPERIMENTAL_* env kill-switch AND an explicit per-repo `true` are set (no +# GITTENSORY_REVIEW_REPOS allowlist fallback -- unlike `features:`, there is no default-on path). `gittensor` +# is the first plugin: gittensory's original subnet mining-registry/scoring integration (per-repo emission +# share, maintainer cut, label multipliers pulled from the gittensor subnet's registry), now opt-in rather +# than a core dependency -- a self-host instance that never sets this has zero footprint from it: no fetch +# from the gittensor subnet registry, no local tracking/backfill of other repos on that subnet. Future +# plugins land in this same block as gittensory broadens beyond gittensor. +# experimental: +# gittensor: true + # Registry-review lane (#2435): lets a self-hosted maintainer point gittensory at their OWN structured # registry (e.g. a subnet/plugin/package catalog) without a gittensory code change -- reviewing additions # to a data file the same way it reviews code. Uncomment and set at least entryFileGlob + collectionField diff --git a/packages/gittensory-engine/src/focus-manifest.ts b/packages/gittensory-engine/src/focus-manifest.ts index 8e84564e2f..0e2abf26c1 100644 --- a/packages/gittensory-engine/src/focus-manifest.ts +++ b/packages/gittensory-engine/src/focus-manifest.ts @@ -243,6 +243,21 @@ export type ConvergedFeatureKey = (typeof CONVERGED_FEATURE_KEYS)[number]; * `GITTENSORY_REVIEW_REPOS` allowlist default, so an operator who sets nothing keeps today's behavior. */ export type FocusManifestFeaturesConfig = { present: boolean } & Record; +/** Optional ecosystem/network integrations under the `experimental:` block — plugins that couple gittensory to + * an external system rather than core review behavior. Starts with `gittensor` (the subnet mining-registry/ + * scoring integration gittensory originally shipped with); future plugins land in this same array as the + * product broadens beyond gittensor. Deliberately a SEPARATE block from `features:` (converged review + * capabilities) — an operator (especially self-host) should be able to see at a glance which toggles are "how + * gittensory reviews PRs" vs "which external network/ecosystem this instance opts into." */ +export const EXPERIMENTAL_PLUGIN_KEYS = ["gittensor"] as const; +export type ExperimentalPluginKey = (typeof EXPERIMENTAL_PLUGIN_KEYS)[number]; + +/** Per-repo activation for `experimental:` plugins. Unlike `features:`, there is no `GITTENSORY_REVIEW_REPOS` + * allowlist fallback for ANY key here — every plugin is the "manifestOnly" precedence shape + * (`resolveManifestOnlyFeature`): OFF unless the operator's global env kill-switch AND an explicit per-repo + * `true` are BOTH set. So an instance that never opts in has zero footprint from any experimental plugin. */ +export type FocusManifestExperimentalConfig = { present: boolean } & Record; + /** * Per-repo registry-review lane configuration (`contentLane:` block, #2435) — lets a self-hosted maintainer * configure their OWN registry (structural file-scope patterns + entry-count cap + dedup fields) without a @@ -882,6 +897,7 @@ export type FocusManifest = { settings: FocusManifestSettings; review: FocusManifestReviewConfig; features: FocusManifestFeaturesConfig; + experimental: FocusManifestExperimentalConfig; contentLane: FocusManifestContentLaneConfig; repoDocGeneration: FocusManifestRepoDocGenerationConfig; reviewRecap: FocusManifestReviewRecapConfig; @@ -981,6 +997,11 @@ const EMPTY_FEATURES_CONFIG: FocusManifestFeaturesConfig = { improvementSignal: null, }; +const EMPTY_EXPERIMENTAL_CONFIG: FocusManifestExperimentalConfig = { + present: false, + gittensor: null, +}; + const EMPTY_CONTENT_LANE_CONFIG: FocusManifestContentLaneConfig = { present: false, entryFileGlob: null, @@ -1034,6 +1055,7 @@ const EMPTY_MANIFEST: FocusManifest = { settings: {}, review: { present: false, footerText: null, note: null, fields: {}, enrichmentAnalyzers: {}, profile: null, tone: null, securityFocus: null, inlineComments: null, fixHandoff: null, autoMergeSummary: null, suggestions: null, changedFilesSummary: null, effortScore: null, impactMap: null, cultureProfile: null, selftune: null, reviewMemory: null, findingCategories: null, inlineCommentsPerCategory: null, minFindingSeverity: null, maxFindings: { ...EMPTY_MAX_FINDINGS_CONFIG }, commentVerbosity: null, e2eTestDelivery: null, e2eTestAutoTrigger: null, pathInstructions: [], instructions: null, excludePaths: [], pathFilters: [], preMergeChecks: [], autoReview: { ...EMPTY_AUTO_REVIEW_CONFIG }, aiModel: { ...EMPTY_SELF_HOST_AI_MODEL_CONFIG }, visual: { ...EMPTY_VISUAL_CONFIG }, linkedIssueSatisfaction: null, sharedConfigSource: null }, features: { ...EMPTY_FEATURES_CONFIG }, + experimental: { ...EMPTY_EXPERIMENTAL_CONFIG }, contentLane: { ...EMPTY_CONTENT_LANE_CONFIG }, repoDocGeneration: { ...EMPTY_REPO_DOC_GENERATION_CONFIG }, reviewRecap: { ...EMPTY_REVIEW_RECAP_CONFIG }, @@ -1065,6 +1087,7 @@ function emptyManifest(source: FocusManifestSource, warnings: string[] = []): Fo settings: {}, review: { present: false, footerText: null, note: null, fields: {}, enrichmentAnalyzers: {}, profile: null, tone: null, securityFocus: null, inlineComments: null, fixHandoff: null, autoMergeSummary: null, suggestions: null, changedFilesSummary: null, effortScore: null, impactMap: null, cultureProfile: null, selftune: null, reviewMemory: null, findingCategories: null, inlineCommentsPerCategory: null, minFindingSeverity: null, maxFindings: { ...EMPTY_MAX_FINDINGS_CONFIG }, commentVerbosity: null, e2eTestDelivery: null, e2eTestAutoTrigger: null, pathInstructions: [], instructions: null, excludePaths: [], pathFilters: [], preMergeChecks: [], autoReview: { ...EMPTY_AUTO_REVIEW_CONFIG }, aiModel: { ...EMPTY_SELF_HOST_AI_MODEL_CONFIG }, visual: { ...EMPTY_VISUAL_CONFIG }, linkedIssueSatisfaction: null, sharedConfigSource: null }, features: { ...EMPTY_FEATURES_CONFIG }, + experimental: { ...EMPTY_EXPERIMENTAL_CONFIG }, contentLane: { ...EMPTY_CONTENT_LANE_CONFIG }, repoDocGeneration: { ...EMPTY_REPO_DOC_GENERATION_CONFIG }, reviewRecap: { ...EMPTY_REVIEW_RECAP_CONFIG }, @@ -1492,6 +1515,39 @@ export function featuresConfigToJson(features: FocusManifestFeaturesConfig): Jso return out; } +/** + * Parse the optional `experimental:` mapping — per-repo activation for optional ecosystem/network plugins + * (starting with `gittensor`, the subnet mining/scoring integration). Mirrors parseFeaturesConfig's shape and + * validation; kept as a SEPARATE top-level block from `features:` so plugin integrations that couple gittensory + * to an external network stay visibly distinct from the converged REVIEW capabilities `features:` toggles, and + * so future plugins land in the same place without touching `features:`'s semantics. + */ +function parseExperimentalConfig(value: JsonValue | undefined, warnings: string[]): FocusManifestExperimentalConfig { + const experimental: FocusManifestExperimentalConfig = { ...EMPTY_EXPERIMENTAL_CONFIG }; + if (value === undefined || value === null) return experimental; + if (typeof value !== "object" || Array.isArray(value)) { + warnings.push('Manifest "experimental" must be a mapping; ignoring it.'); + return experimental; + } + const record = value as Record; + for (const key of EXPERIMENTAL_PLUGIN_KEYS) { + experimental[key] = normalizeOptionalBoolean(record[key], `experimental.${key}`, warnings); + } + experimental.present = EXPERIMENTAL_PLUGIN_KEYS.some((key) => experimental[key] !== null); + return experimental; +} + +/** Serialize an experimental config back into the parse-compatible `experimental:` shape so a cached snapshot + * round-trips through {@link parseExperimentalConfig} unchanged. Returns null when nothing is configured. */ +export function experimentalConfigToJson(experimental: FocusManifestExperimentalConfig): JsonValue { + if (!experimental.present) return null; + const out: Record = {}; + for (const key of EXPERIMENTAL_PLUGIN_KEYS) { + if (experimental[key] !== null) out[key] = experimental[key]; + } + return out; +} + /** A positive INTEGER count (not a score/confidence) — e.g. `contentLane.maxAppendedEntries` counts discrete * surfaces[] entries, so a fractional value (a likely typo) would render a nonsensical contributor-facing close * message ("append between 1 and 2.5 entries"). Rejects fractional and non-positive values alike. */ @@ -3025,6 +3081,7 @@ export function parseFocusManifest(raw: unknown, source?: FocusManifestSource): settings: parseSettingsOverride(record.settings, warnings, resolvedSource), review: parseReviewConfig(record.review, warnings), features: parseFeaturesConfig(record.features, warnings), + experimental: parseExperimentalConfig(record.experimental, warnings), contentLane: parseContentLaneConfig(record.contentLane, warnings), repoDocGeneration: parseRepoDocGenerationConfig(record.repoDocGeneration, warnings), reviewRecap: parseReviewRecapConfig(record.reviewRecap, warnings), @@ -3043,6 +3100,7 @@ export function parseFocusManifest(raw: unknown, source?: FocusManifestSource): Object.keys(manifest.settings).length === 0 && !manifest.review.present && !manifest.features.present && + !manifest.experimental.present && !manifest.contentLane.present && !manifest.repoDocGeneration.present && !manifest.reviewRecap.present && diff --git a/packages/gittensory-engine/src/index.ts b/packages/gittensory-engine/src/index.ts index f6800de26f..4a5935784f 100644 --- a/packages/gittensory-engine/src/index.ts +++ b/packages/gittensory-engine/src/index.ts @@ -492,6 +492,7 @@ export { export { compileFocusManifestPolicy, contentLaneConfigToJson, + experimentalConfigToJson, featuresConfigToJson, formatManifestValidationNotice, gateConfigToJson, @@ -507,6 +508,7 @@ export { settingsOverrideToJson, MAX_FOCUS_MANIFEST_BYTES, CONVERGED_FEATURE_KEYS, + EXPERIMENTAL_PLUGIN_KEYS, COMMENT_VERBOSITY_LEVELS, EMPTY_AUTO_REVIEW_CONFIG, EMPTY_MAX_FINDINGS_CONFIG, @@ -519,8 +521,10 @@ export { type AutoReviewConfig, type CommentVerbosity, type ConvergedFeatureKey, + type ExperimentalPluginKey, type FocusManifest, type FocusManifestContentLaneConfig, + type FocusManifestExperimentalConfig, type FocusManifestFeaturesConfig, type FocusManifestGateConfig, type FocusManifestIssueDiscoveryPolicy, diff --git a/src/env.d.ts b/src/env.d.ts index 102700e90e..eb98c876cb 100644 --- a/src/env.d.ts +++ b/src/env.d.ts @@ -381,6 +381,14 @@ declare global { * recording are wired, reading a promoted override into the live gate is a noted follow-up that must not * risk loosening the gate. See src/review/selftune-wire.ts. */ GITTENSORY_REVIEW_SELFTUNE?: string; + /** Experimental `gittensor` plugin (the `experimental:` manifest block, first key): the operator-level + * kill-switch for gittensory's original subnet mining-registry/scoring integration, now opt-in rather than + * a core dependency. ANDed with the per-repo `.gittensory.yml experimental.gittensor` opt-in -- neither + * alone is sufficient, and unlike `features:` there is no GITTENSORY_REVIEW_REPOS allowlist fallback. + * Default OFF -- flag-OFF (or every repo unset), refresh-registry is never enqueued (see src/index.ts) and + * a self-host box makes zero outbound contact with the gittensor subnet registry. See + * src/review/gittensor-wire.ts. */ + GITTENSORY_EXPERIMENTAL_GITTENSOR?: string; /** Maintainer recap digest (#1963, #2248): when truthy, a cross-repo RecapReport -- gittensory's OWN * gate-precision + outcome-calibration data folded across every scanned repo (buildMaintainerRecap, * #2239) -- is delivered to Discord on a cron cadence. GITTENSORY_RECAP_CADENCE ("daily" | "weekly", diff --git a/src/index.ts b/src/index.ts index d944c9c3b9..e58a5f520b 100644 --- a/src/index.ts +++ b/src/index.ts @@ -5,6 +5,7 @@ import { processDlqBatch } from "./queue/dlq"; import { processJob } from "./queue/processors"; import { isOrbBrokerEnabled } from "./orb/broker"; import { isOrbBrokerMode } from "./orb/broker-client"; +import { gittensorEnabledRepoFullNames } from "./review/gittensor-wire"; import { isOpsEnabled } from "./review/ops-wire"; import { isRecapEnabled, resolveMaintainerRecapManifestOverride, shouldFireMaintainerRecap } from "./review/maintainer-recap-wire"; import { isSweepWatchdogEnabled } from "./review/sweep-watchdog"; @@ -202,7 +203,19 @@ async function enqueueScheduledJobs(env: Env, controller: ScheduledController): // ZERO new work and the enqueued set is byte-identical to today. if (selfHostedReviews && isReconciliationWindow && isPrReconciliationEnabled(env)) jobs.push({ type: "reconcile-open-prs", requestedBy: "schedule" }); if (isHourly) { - jobs.push({ type: "refresh-registry", requestedBy: "schedule" }); + // Isolation (#experimental-gittensor-plugin): on self-host, refresh-registry both FETCHES from and + // PERSISTS the whole upstream gittensor-subnet registry (entrius/gittensor has no server-side filtering, + // and persistRegistrySnapshot's own self-host scoping — see registry/sync.ts — narrows what gets WRITTEN + // locally but can't narrow what gets fetched). Skip enqueuing the job entirely when this instance has no + // repo opted into the experimental `gittensor` plugin, so a plain self-host box makes ZERO outbound + // contact with the subnet registry. Cloud is unaffected — always enqueues, exactly like before this + // narrowing existed. + const gittensorOptedIn = selfHostedReviews ? await gittensorEnabledRepoFullNames(env) : null; + if (!selfHostedReviews || (gittensorOptedIn && gittensorOptedIn.size > 0)) { + jobs.push({ type: "refresh-registry", requestedBy: "schedule" }); + } else { + console.log(JSON.stringify({ event: "refresh_registry_skipped_no_gittensor_opt_in" })); + } // Brokered self-host installed-repo sync (#5028): the central Orb relay deliberately does not forward // installation/installation_repositories events to brokered containers, so a brokered self-host has no // other way to learn its own repo list beyond the first forwarded PR/issue event per repo. Self-host + diff --git a/src/review/gittensor-wire.ts b/src/review/gittensor-wire.ts new file mode 100644 index 0000000000..197df86d1f --- /dev/null +++ b/src/review/gittensor-wire.ts @@ -0,0 +1,59 @@ +// Gittensor experimental-plugin activation wiring. `gittensor` is the first key under the `experimental:` +// manifest block (EXPERIMENTAL_PLUGIN_KEYS) -- gittensory's original subnet mining-registry/scoring +// integration, now an OPT-IN plugin rather than a core dependency, so a self-host instance with no gittensor +// affiliation has zero footprint from it (see registry/sync.ts's self-host scoping, which this feeds, and +// index.ts's cron gate, which skips the registry fetch entirely when nothing is opted in). +// +// Mirrors impact-map-wire.ts's isImpactMapEnabled/shouldComputeImpactMap: a single GLOBAL env kill-switch the +// operator controls, ANDed with an EXPLICIT per-repo `.gittensory.yml experimental.gittensor` manifest `true` +// (resolveManifestOnlyFeature's shape -- no allowlist fallback, unlike the converged `features:` block). Both +// OFF by default: with the env flag unset, no repo is ever treated as gittensor-opted-in. Cloud never consults +// any of this -- registry/sync.ts only calls gittensorEnabledRepoFullNames on the self-host branch, so the +// hosted product's existing full-subnet behavior is untouched. + +import { listRepositories } from "../db/repositories"; +import { loadRepoFocusManifest } from "../signals/focus-manifest-loader"; +import { resolveManifestOnlyFeature } from "./feature-activation"; + +/** True when the gittensor subnet-scoring plugin is enabled at the operator level. Flag-OFF (default) -> no + * repo is ever gittensor-opted-in regardless of what any `.gittensory.yml` says, and + * {@link gittensorEnabledRepoFullNames} short-circuits before reading a single manifest. Truthy follows the + * codebase convention (`/^(1|true|yes|on)$/i`, same as isImpactMapEnabled / isSelfTuneEnabled). */ +export function isGittensorPluginEnabled(env: { GITTENSORY_EXPERIMENTAL_GITTENSOR?: string | undefined }): boolean { + return /^(1|true|yes|on)$/i.test(env.GITTENSORY_EXPERIMENTAL_GITTENSOR ?? ""); +} + +/** Resolve whether the gittensor plugin is active for THIS repo: the operator's global env kill-switch AND an + * explicit per-repo manifest opt-in. Neither alone is sufficient -- mirrors every other manifestOnly feature + * gate in this codebase (env kill-switch first, then the manifest narrows it further). */ +export function shouldEnableGittensorForRepo( + env: { GITTENSORY_EXPERIMENTAL_GITTENSOR?: string | undefined }, + manifestGittensorEnabled: boolean | null | undefined, +): boolean { + return resolveManifestOnlyFeature(isGittensorPluginEnabled(env), manifestGittensorEnabled); +} + +/** + * The set of repos (lowercased full names) this self-host instance has opted into the gittensor plugin for -- + * every locally-known repo (listRepositories, deliberately NOT filtered by isRegistered: isRegistered is + * itself DOWNSTREAM of this decision on self-host, see registry/sync.ts's persistRegistrySnapshot, so filtering + * on it here would be circular -- a repo could never earn its first isRegistered=true) whose manifest sets + * `experimental.gittensor: true` AND the global env kill-switch is on. A per-repo manifest-load error is + * skipped (treated as not-opted-in), never aborts the pass -- mirrors selftune-wire.ts's selfTuneRepos. + * Flag-OFF (default) short-circuits before listing repos or loading a single manifest, so a plain self-host + * instance makes zero local reads for this and zero outbound gittensor-registry requests (see index.ts). + */ +export async function gittensorEnabledRepoFullNames(env: Env & { GITTENSORY_EXPERIMENTAL_GITTENSOR?: string | undefined }): Promise> { + if (!isGittensorPluginEnabled(env)) return new Set(); + const repos = await listRepositories(env); + const enabled = new Set(); + for (const repo of repos) { + try { + const manifest = await loadRepoFocusManifest(env, repo.fullName); + if (shouldEnableGittensorForRepo(env, manifest.experimental.gittensor)) enabled.add(repo.fullName.toLowerCase()); + } catch { + /* a manifest-load blip on one repo must not block the rest of the pass */ + } + } + return enabled; +} diff --git a/src/signals/focus-manifest-loader.ts b/src/signals/focus-manifest-loader.ts index e1e860a8af..592579892c 100644 --- a/src/signals/focus-manifest-loader.ts +++ b/src/signals/focus-manifest-loader.ts @@ -1,7 +1,7 @@ import { listSignalSnapshots, persistSignalSnapshot } from "../db/repositories"; import type { JsonValue } from "../types"; import { nowIso } from "../utils/json"; -import { contentLaneConfigToJson, featuresConfigToJson, gateConfigToJson, MAX_FOCUS_MANIFEST_BYTES, parseFocusManifest, parseFocusManifestContent, repoDocGenerationConfigToJson, reviewConfigToJson, reviewRecapConfigToJson, maintainerRecapConfigToJson, settingsOverrideToJson, type FocusManifest, type FocusManifestSource, type RepoReviewContext } from "./focus-manifest"; +import { contentLaneConfigToJson, experimentalConfigToJson, featuresConfigToJson, gateConfigToJson, MAX_FOCUS_MANIFEST_BYTES, parseFocusManifest, parseFocusManifestContent, repoDocGenerationConfigToJson, reviewConfigToJson, reviewRecapConfigToJson, maintainerRecapConfigToJson, settingsOverrideToJson, type FocusManifest, type FocusManifestSource, type RepoReviewContext } from "./focus-manifest"; import { GITTENSORY_REPO_FOCUS_MANIFEST_YAML, resolveGittensorySelfRepoFullName } from "../config/gittensory-repo-focus-manifest"; import type { LocalManifestLoadResult } from "../selfhost/private-config"; @@ -299,6 +299,7 @@ function manifestToJson(manifest: FocusManifest): Record { settings: settingsOverrideToJson(manifest.settings), review: reviewConfigToJson(manifest.review), features: featuresConfigToJson(manifest.features), + experimental: experimentalConfigToJson(manifest.experimental), contentLane: contentLaneConfigToJson(manifest.contentLane), repoDocGeneration: repoDocGenerationConfigToJson(manifest.repoDocGeneration), reviewRecap: reviewRecapConfigToJson(manifest.reviewRecap), diff --git a/src/signals/focus-manifest.ts b/src/signals/focus-manifest.ts index cf509de45b..13d3384fbd 100644 --- a/src/signals/focus-manifest.ts +++ b/src/signals/focus-manifest.ts @@ -7,6 +7,7 @@ export { AI_REVIEW_CADENCES, COMMENT_VERBOSITY_LEVELS, CONVERGED_FEATURE_KEYS, + EXPERIMENTAL_PLUGIN_KEYS, E2E_TEST_DELIVERY_MODES, EMPTY_AUTO_REVIEW_CONFIG, EMPTY_MAX_FINDINGS_CONFIG, @@ -19,6 +20,7 @@ export { REVIEW_PROFILES, compileFocusManifestPolicy, contentLaneConfigToJson, + experimentalConfigToJson, featuresConfigToJson, formatManifestValidationNotice, gateConfigToJson, @@ -39,8 +41,10 @@ export { type CommentVerbosity, type ConvergedFeatureKey, type E2eTestDeliveryMode, + type ExperimentalPluginKey, type FocusManifest, type FocusManifestContentLaneConfig, + type FocusManifestExperimentalConfig, type FocusManifestFeaturesConfig, type FocusManifestFinding, type FocusManifestGateConfig, diff --git a/test/helpers/d1.ts b/test/helpers/d1.ts index 6c98bd7874..d7d794aab5 100644 --- a/test/helpers/d1.ts +++ b/test/helpers/d1.ts @@ -131,6 +131,10 @@ export function createTestEnv(overrides: Partial = {}): Env { // Default-ON in production (settings/automation-bot-skip.ts); most tests don't involve a bot actor at // all, so this default doesn't change their behavior. Tests exercising this feature override it directly. GITTENSORY_SKIP_AUTOMATION_BOT_PRS: "true", + // Default OFF, matching wrangler.jsonc — a new required `vars` entry needs an explicit base value here + // (Partial alone leaves it optional under exactOptionalPropertyTypes, which Env's required field + // rejects). Tests exercising the experimental gittensor plugin override it directly. + GITTENSORY_EXPERIMENTAL_GITTENSOR: "false", ...overrides, }; } diff --git a/test/unit/focus-manifest.test.ts b/test/unit/focus-manifest.test.ts index 6f12e31ce1..d3b5e939da 100644 --- a/test/unit/focus-manifest.test.ts +++ b/test/unit/focus-manifest.test.ts @@ -6,6 +6,7 @@ import { compileFocusManifestPolicy, contentLaneConfigToJson, deriveContributionLanes, + experimentalConfigToJson, featuresConfigToJson, gateConfigToJson, isFocusManifestPublicSafe, @@ -833,6 +834,7 @@ describe("compileFocusManifestPolicy", () => { settings: {}, review: { present: false, footerText: null, note: null, fields: {}, enrichmentAnalyzers: {}, profile: null, tone: null, securityFocus: null, inlineComments: null, fixHandoff: null, autoMergeSummary: null, suggestions: null, changedFilesSummary: null, effortScore: null, impactMap: null, cultureProfile: null, selftune: null, reviewMemory: null, findingCategories: null, inlineCommentsPerCategory: null, minFindingSeverity: null, maxFindings: { blockers: null, nits: null }, commentVerbosity: null, e2eTestDelivery: null, e2eTestAutoTrigger: null, pathInstructions: [], instructions: null, excludePaths: [], pathFilters: [], preMergeChecks: [], autoReview: { ...EMPTY_AUTO_REVIEW_CONFIG }, aiModel: { ...EMPTY_SELF_HOST_AI_MODEL_CONFIG }, visual: { ...EMPTY_VISUAL_CONFIG }, linkedIssueSatisfaction: null, sharedConfigSource: null }, features: { present: false, rag: null, reputation: null, unifiedComment: null, safety: null, grounding: null, e2eTests: null, screenshots: null, improvementSignal: null }, + experimental: { present: false, gittensor: null }, contentLane: { present: false, entryFileGlob: null, providerFileGlob: null, artifactGlob: null, collectionField: null, maxAppendedEntries: null, duplicateKeyFields: [], validatorId: null }, repoDocGeneration: { present: false, enabled: false, scope: ["agents"], allowOverwriteExisting: false, refreshIntervalDays: 7 }, reviewRecap: { present: false, enabled: false, cadenceDays: 7 }, @@ -1578,6 +1580,26 @@ describe("parseFocusManifest gate config", () => { expect(featuresConfigToJson(parseFocusManifest({ features: {} }).features)).toBeNull(); }); + it("parses the experimental: block (opt-in ecosystem/network plugins), round-trips it, and makes the manifest present", () => { + const m = parseFocusManifest({ experimental: { gittensor: true } }); + expect(m.present).toBe(true); + expect(m.experimental.present).toBe(true); + expect(m.experimental.gittensor).toBe(true); + // Round-trips through experimentalConfigToJson → parseFocusManifest unchanged. + expect(parseFocusManifest({ experimental: experimentalConfigToJson(m.experimental) }).experimental).toEqual(m.experimental); + const off = parseFocusManifest({ experimental: { gittensor: false } }); + expect(off.experimental.gittensor).toBe(false); + expect(experimentalConfigToJson(off.experimental)).toEqual({ gittensor: false }); + // A non-boolean value warns and is dropped (stays null); a non-mapping warns. + expect(parseFocusManifest({ experimental: { gittensor: "yes" } }).warnings.some((w) => /experimental\.gittensor/.test(w))).toBe(true); + expect(parseFocusManifest({ experimental: ["nope"] }).warnings.some((w) => /"experimental" must be a mapping/.test(w))).toBe(true); + // Unset stays null (⇒ the manifestOnly resolver treats it as no opt-in). + expect(parseFocusManifest({}).experimental.gittensor).toBeNull(); + // An empty experimental block leaves the manifest absent (no recognized fields). + expect(parseFocusManifest({ experimental: {} }).experimental.present).toBe(false); + expect(experimentalConfigToJson(parseFocusManifest({ experimental: {} }).experimental)).toBeNull(); + }); + it("parses the contentLane: block (#2435 per-repo registry-lane config), round-trips it, and makes the manifest present", () => { const m = parseFocusManifest({ contentLane: { diff --git a/test/unit/gittensor-wire.test.ts b/test/unit/gittensor-wire.test.ts new file mode 100644 index 0000000000..7cf6ca6d6c --- /dev/null +++ b/test/unit/gittensor-wire.test.ts @@ -0,0 +1,107 @@ +import { describe, expect, it } from "vitest"; +import { gittensorEnabledRepoFullNames, isGittensorPluginEnabled, shouldEnableGittensorForRepo } from "../../src/review/gittensor-wire"; +import { upsertRepoFocusManifest } from "../../src/signals/focus-manifest-loader"; +import { createTestEnv } from "../helpers/d1"; + +async function seedRegisteredRepo(env: Env, fullName: string): Promise { + const [owner, name] = fullName.split("/"); + await (env.DB as unknown as { prepare: (s: string) => { bind: (...v: unknown[]) => { run: () => Promise } } }) + .prepare("INSERT INTO repositories (full_name, owner, name, is_installed, is_registered) VALUES (?, ?, ?, 1, 0)") + .bind(fullName, owner, name) + .run(); +} + +// Wrap env.DB.prepare so any SQL matching `pattern` throws, exercising a fail-safe catch; every other query +// delegates to the real test DB unchanged. Mirrors selftune-wiring.test.ts's poisonDbPrepare. +function poisonDbPrepare(env: Env, pattern: RegExp): void { + const realPrepare = env.DB.prepare.bind(env.DB); + env.DB.prepare = ((sql: string) => { + if (pattern.test(sql)) throw new Error("poisoned query"); + return realPrepare(sql); + }) as typeof env.DB.prepare; +} + +describe("isGittensorPluginEnabled", () => { + it("is OFF for unset/false and ON for the truthy convention", () => { + expect(isGittensorPluginEnabled({})).toBe(false); + expect(isGittensorPluginEnabled({ GITTENSORY_EXPERIMENTAL_GITTENSOR: "false" })).toBe(false); + expect(isGittensorPluginEnabled({ GITTENSORY_EXPERIMENTAL_GITTENSOR: "true" })).toBe(true); + expect(isGittensorPluginEnabled({ GITTENSORY_EXPERIMENTAL_GITTENSOR: "1" })).toBe(true); + expect(isGittensorPluginEnabled({ GITTENSORY_EXPERIMENTAL_GITTENSOR: "on" })).toBe(true); + expect(isGittensorPluginEnabled({ GITTENSORY_EXPERIMENTAL_GITTENSOR: "yes" })).toBe(true); + }); +}); + +describe("shouldEnableGittensorForRepo", () => { + it("requires BOTH the operator env flag AND the per-repo manifest opt-in", () => { + expect(shouldEnableGittensorForRepo({ GITTENSORY_EXPERIMENTAL_GITTENSOR: "true" }, true)).toBe(true); + }); + + it("is OFF when the operator flag is on but the manifest didn't opt in", () => { + expect(shouldEnableGittensorForRepo({ GITTENSORY_EXPERIMENTAL_GITTENSOR: "true" }, false)).toBe(false); + expect(shouldEnableGittensorForRepo({ GITTENSORY_EXPERIMENTAL_GITTENSOR: "true" }, null)).toBe(false); + expect(shouldEnableGittensorForRepo({ GITTENSORY_EXPERIMENTAL_GITTENSOR: "true" }, undefined)).toBe(false); + }); + + it("is OFF when the manifest opted in but the operator flag is off (repo cannot self-enable)", () => { + expect(shouldEnableGittensorForRepo({ GITTENSORY_EXPERIMENTAL_GITTENSOR: "false" }, true)).toBe(false); + expect(shouldEnableGittensorForRepo({}, true)).toBe(false); + }); + + it("is OFF when both are off", () => { + expect(shouldEnableGittensorForRepo({}, false)).toBe(false); + }); +}); + +describe("gittensorEnabledRepoFullNames", () => { + it("short-circuits to an empty set when the operator flag is off, without reading the repositories table", () => { + const env = createTestEnv(); + // If this ever queried the DB despite the flag being off, the poisoned "repositories" query would throw + // and this test would fail with an unhandled rejection instead of resolving cleanly. + poisonDbPrepare(env, /"repositories"/i); + return expect(gittensorEnabledRepoFullNames(env)).resolves.toEqual(new Set()); + }); + + it("is empty when the flag is on but no repos are locally known", async () => { + const env = createTestEnv({ GITTENSORY_EXPERIMENTAL_GITTENSOR: "true" }); + await expect(gittensorEnabledRepoFullNames(env)).resolves.toEqual(new Set()); + }); + + it("excludes a repo whose manifest never sets experimental.gittensor", async () => { + const env = createTestEnv({ GITTENSORY_EXPERIMENTAL_GITTENSOR: "true" }); + await seedRegisteredRepo(env, "owner/unopted"); + await upsertRepoFocusManifest(env, "owner/unopted", { wantedPaths: ["src/"] }); + await expect(gittensorEnabledRepoFullNames(env)).resolves.toEqual(new Set()); + }); + + it("excludes a repo that explicitly sets experimental.gittensor: false", async () => { + const env = createTestEnv({ GITTENSORY_EXPERIMENTAL_GITTENSOR: "true" }); + await seedRegisteredRepo(env, "owner/optedout"); + await upsertRepoFocusManifest(env, "owner/optedout", { experimental: { gittensor: false } }); + await expect(gittensorEnabledRepoFullNames(env)).resolves.toEqual(new Set()); + }); + + it("includes only the repos that explicitly opt in, lowercased, out of a mixed set", async () => { + const env = createTestEnv({ GITTENSORY_EXPERIMENTAL_GITTENSOR: "true" }); + await seedRegisteredRepo(env, "Owner/OptedIn"); + await upsertRepoFocusManifest(env, "Owner/OptedIn", { experimental: { gittensor: true } }); + await seedRegisteredRepo(env, "owner/other"); + await upsertRepoFocusManifest(env, "owner/other", { experimental: { gittensor: false } }); + await expect(gittensorEnabledRepoFullNames(env)).resolves.toEqual(new Set(["owner/optedin"])); + }); + + it("is not gated by isRegistered — a not-yet-registered but opted-in repo is still included (avoids the chicken-and-egg deadlock)", async () => { + const env = createTestEnv({ GITTENSORY_EXPERIMENTAL_GITTENSOR: "true" }); + await seedRegisteredRepo(env, "owner/notyetregistered"); // seeded with is_registered=0 + await upsertRepoFocusManifest(env, "owner/notyetregistered", { experimental: { gittensor: true } }); + await expect(gittensorEnabledRepoFullNames(env)).resolves.toEqual(new Set(["owner/notyetregistered"])); + }); + + it("fails safe per-repo: a manifest-load error is swallowed and the pass still resolves", async () => { + const env = createTestEnv({ GITTENSORY_EXPERIMENTAL_GITTENSOR: "true" }); + await seedRegisteredRepo(env, "owner/blip"); + // loadRepoFocusManifest's cache read hits signal_snapshots; poison it so the per-repo try/catch fires. + poisonDbPrepare(env, /"signal_snapshots"/i); + await expect(gittensorEnabledRepoFullNames(env)).resolves.toEqual(new Set()); + }); +}); diff --git a/test/unit/index.test.ts b/test/unit/index.test.ts index 7c0a82d0ab..6b744b53b4 100644 --- a/test/unit/index.test.ts +++ b/test/unit/index.test.ts @@ -572,19 +572,44 @@ describe("worker entrypoint", () => { await worker.scheduled(controllerFor("2026-05-25T05:00:00.000Z"), env, executionContext(waitUntil)); await Promise.all(waitUntil); + // refresh-registry is absent: a fresh self-host instance (this test's default createTestEnv) has no repo + // opted into the experimental gittensor plugin, so the job is never enqueued (#experimental-gittensor-plugin) + // — see "re-includes refresh-registry once a repo opts into the experimental gittensor plugin" below. expect(sent).toEqual([ { type: "agent-regate-sweep", requestedBy: "schedule" }, { type: "backfill-registered-repos", requestedBy: "schedule", mode: "light" }, { type: "repair-data-fidelity", requestedBy: "schedule" }, { type: "refresh-installation-health", requestedBy: "schedule" }, { type: "backlog-convergence-sweep", requestedBy: "schedule" }, - { type: "refresh-registry", requestedBy: "schedule" }, { type: "refresh-scoring-model", requestedBy: "schedule" }, { type: "refresh-upstream-drift", requestedBy: "schedule" }, { type: "rollup-product-usage", requestedBy: "schedule", days: 7 }, ]); }); + it("re-includes refresh-registry once a repo opts into the experimental gittensor plugin", async () => { + const sent: Array = []; + const env = createTestEnv({ + GITTENSORY_EXPERIMENTAL_GITTENSOR: "true", + JOBS: { + async send(message: import("../../src/types").JobMessage) { + sent.push(message); + }, + } as unknown as Queue, + }); + await (env.DB as unknown as { prepare: (s: string) => { bind: (...v: unknown[]) => { run: () => Promise } } }) + .prepare("INSERT INTO repositories (full_name, owner, name, is_installed, is_registered) VALUES (?, ?, ?, 1, 0)") + .bind("JSONbored/gittensory", "JSONbored", "gittensory") + .run(); + await (await import("../../src/signals/focus-manifest-loader")).upsertRepoFocusManifest(env, "JSONbored/gittensory", { experimental: { gittensor: true } }); + const waitUntil: Promise[] = []; + + await worker.scheduled(controllerFor("2026-05-25T05:00:00.000Z"), env, executionContext(waitUntil)); + await Promise.all(waitUntil); + + expect(sent.some((message) => message.type === "refresh-registry")).toBe(true); + }); + it("enqueues full-sync scheduled work every six hours", async () => { const sent: Array = []; const env = createTestEnv({ @@ -599,13 +624,13 @@ describe("worker entrypoint", () => { await worker.scheduled(controllerFor("2026-05-25T06:00:00.000Z"), env, executionContext(waitUntil)); await Promise.all(waitUntil); + // refresh-registry is absent here too — see the comment on "enqueues hourly refreshes..." above. expect(sent).toEqual([ { type: "agent-regate-sweep", requestedBy: "schedule" }, { type: "backfill-registered-repos", requestedBy: "schedule", mode: "full" }, { type: "repair-data-fidelity", requestedBy: "schedule" }, { type: "refresh-installation-health", requestedBy: "schedule" }, { type: "backlog-convergence-sweep", requestedBy: "schedule" }, - { type: "refresh-registry", requestedBy: "schedule" }, { type: "refresh-scoring-model", requestedBy: "schedule" }, { type: "refresh-upstream-drift", requestedBy: "schedule" }, { type: "rollup-product-usage", requestedBy: "schedule", days: 7 }, @@ -643,13 +668,13 @@ describe("worker entrypoint", () => { await Promise.all(waitUntil); // The enqueued SET is unchanged — jitter only spreads run_after timing, never which jobs are sent. + // refresh-registry is absent here too — see the comment on "enqueues hourly refreshes..." above. expect(sent.map((s) => s.message.type)).toEqual([ "agent-regate-sweep", "backfill-registered-repos", "repair-data-fidelity", "refresh-installation-health", "backlog-convergence-sweep", - "refresh-registry", "refresh-scoring-model", "refresh-upstream-drift", "rollup-product-usage", diff --git a/worker-configuration.d.ts b/worker-configuration.d.ts index ff5da0d2e0..b3c449102c 100644 --- a/worker-configuration.d.ts +++ b/worker-configuration.d.ts @@ -1,5 +1,5 @@ /* eslint-disable */ -// Generated by Wrangler by running `wrangler types` (hash: 31e2a5aa781186e51d85d560f8a9f25b) +// Generated by Wrangler by running `wrangler types` (hash: e01a02a550c5fe9e8fe3625c9f9338bf) // Runtime types generated with workerd@1.20260701.1 2026-05-28 nodejs_compat interface __BaseEnv_Env { DB: D1Database; @@ -35,6 +35,7 @@ interface __BaseEnv_Env { GITTENSORY_REVIEW_MEMORY: "false"; GITTENSORY_REVIEW_CONTENT_LANE: "false"; GITTENSORY_REVIEW_SELFTUNE: "false"; + GITTENSORY_EXPERIMENTAL_GITTENSOR: "false"; GITTENSORY_MAINTAINER_RECAP: "false"; GITHUB_STATUS_ROLLUP_GRAPHQL: "false"; GITTENSORY_REVIEW_PLANNER: "false"; @@ -73,6 +74,7 @@ declare namespace NodeJS { | "GITTENSORY_AUTO_FILE_DRIFT_ISSUES" | "GITTENSORY_DRIFT_ISSUE_REPO" | "GITTENSORY_DUPLICATE_WINNER" + | "GITTENSORY_EXPERIMENTAL_GITTENSOR" | "GITTENSORY_MAINTAINER_RECAP" | "GITTENSORY_OPEN_PR_FILE_COLLISION" | "GITTENSORY_PR_RECONCILIATION" diff --git a/wrangler.jsonc b/wrangler.jsonc index 80430c3e85..7f1011ebe3 100644 --- a/wrangler.jsonc +++ b/wrangler.jsonc @@ -141,6 +141,14 @@ // identical to today. Config-application (reading a promoted override into the live gate) is a deferred // follow-up — see src/review/selftune-wire.ts. "GITTENSORY_REVIEW_SELFTUNE": "false", + // Experimental `gittensor` plugin (the `experimental:` manifest block, first key): the operator-level + // kill-switch for gittensory's original subnet mining-registry/scoring integration, now opt-in rather than + // a core dependency. ANDed with the per-repo `.gittensory.yml experimental.gittensor` opt-in — neither + // alone is sufficient, and unlike `features:` there is no GITTENSORY_REVIEW_REPOS allowlist fallback. + // Default OFF — flag-OFF (or every repo unset), refresh-registry is never enqueued (see src/index.ts), + // registry/sync.ts persists nothing for this instance, and a self-host box makes zero outbound contact + // with the gittensor subnet registry. See src/review/gittensor-wire.ts. + "GITTENSORY_EXPERIMENTAL_GITTENSOR": "false", // Maintainer recap digest (#1963, #2248): deliver a cross-repo RecapReport -- gittensory's own gate- // precision + outcome-calibration data folded across every scanned repo -- to Discord on a cron cadence // (GITTENSORY_RECAP_CADENCE=daily|weekly, default weekly; GITTENSORY_RECAP_HOUR / GITTENSORY_RECAP_DAY From 284dfbc5e3dfaccf8bec1f5e71d60ce657e44c0f Mon Sep 17 00:00:00 2001 From: JSONbored <49853598+JSONbored@users.noreply.github.com> Date: Sat, 11 Jul 2026 02:43:30 -0700 Subject: [PATCH 2/3] test(review): close codecov/patch branch gaps in the gittensor plugin toggle MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Covers the cloud-mode short-circuit in index.ts's refresh-registry cron gate (never exercised — every prior test ran self-hosted) and the experimental: null / non-array-primitive parse branches in focus-manifest.ts that were logically distinct from the already-tested undefined and array cases. --- test/unit/focus-manifest.test.ts | 7 +++++++ test/unit/index.test.ts | 20 ++++++++++++++++++++ 2 files changed, 27 insertions(+) diff --git a/test/unit/focus-manifest.test.ts b/test/unit/focus-manifest.test.ts index d3b5e939da..74f0eff08a 100644 --- a/test/unit/focus-manifest.test.ts +++ b/test/unit/focus-manifest.test.ts @@ -1598,6 +1598,13 @@ describe("parseFocusManifest gate config", () => { // An empty experimental block leaves the manifest absent (no recognized fields). expect(parseFocusManifest({ experimental: {} }).experimental.present).toBe(false); expect(experimentalConfigToJson(parseFocusManifest({ experimental: {} }).experimental)).toBeNull(); + // An explicit `experimental: null` takes the same early-return path as unset (distinct branch from + // the undefined case above). + expect(parseFocusManifest({ experimental: null }).experimental.gittensor).toBeNull(); + expect(parseFocusManifest({ experimental: null }).experimental.present).toBe(false); + // A non-object, non-array primitive (not just an array) also warns as "must be a mapping". + expect(parseFocusManifest({ experimental: "nope" }).warnings.some((w) => /"experimental" must be a mapping/.test(w))).toBe(true); + expect(parseFocusManifest({ experimental: "nope" }).experimental.present).toBe(false); }); it("parses the contentLane: block (#2435 per-repo registry-lane config), round-trips it, and makes the manifest present", () => { diff --git a/test/unit/index.test.ts b/test/unit/index.test.ts index 6b744b53b4..95b23e2cc8 100644 --- a/test/unit/index.test.ts +++ b/test/unit/index.test.ts @@ -610,6 +610,26 @@ describe("worker entrypoint", () => { expect(sent.some((message) => message.type === "refresh-registry")).toBe(true); }); + it("always enqueues refresh-registry on a cloud (non-self-hosted) runtime, regardless of gittensor opt-in (#experimental-gittensor-plugin)", async () => { + const sent: Array = []; + const env = createTestEnv({ + JOBS: { + async send(message: import("../../src/types").JobMessage) { + sent.push(message); + }, + } as unknown as Queue, + }); + delete env.SELFHOST_TRANSIENT_CACHE; + const waitUntil: Promise[] = []; + + await worker.scheduled(controllerFor("2026-05-25T05:00:00.000Z"), env, executionContext(waitUntil)); + await Promise.all(waitUntil); + + // Cloud never consults gittensorEnabledRepoFullNames at all — the `!selfHostedReviews` short-circuit + // fires before the opted-in check, exactly like before this narrowing existed. + expect(sent.some((message) => message.type === "refresh-registry")).toBe(true); + }); + it("enqueues full-sync scheduled work every six hours", async () => { const sent: Array = []; const env = createTestEnv({ From c686328e21fa2d1e308e2f0c6244c7ef7e59501b Mon Sep 17 00:00:00 2001 From: JSONbored <49853598+JSONbored@users.noreply.github.com> Date: Sat, 11 Jul 2026 02:58:16 -0700 Subject: [PATCH 3/3] test(review): cover experimentalConfigToJson's per-key null-skip branch Pinpointed via Codecov's line-level diff data: with a single-key EXPERIMENTAL_PLUGIN_KEYS, present:true always implies that one key is non-null through the normal parse path, so the loop's false branch was unreachable via parseFocusManifest. The function is exported and pure over the full type, so exercise it directly with a hand-crafted config. --- test/unit/focus-manifest.test.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/test/unit/focus-manifest.test.ts b/test/unit/focus-manifest.test.ts index 74f0eff08a..42d92cdc0e 100644 --- a/test/unit/focus-manifest.test.ts +++ b/test/unit/focus-manifest.test.ts @@ -1605,6 +1605,11 @@ describe("parseFocusManifest gate config", () => { // A non-object, non-array primitive (not just an array) also warns as "must be a mapping". expect(parseFocusManifest({ experimental: "nope" }).warnings.some((w) => /"experimental" must be a mapping/.test(w))).toBe(true); expect(parseFocusManifest({ experimental: "nope" }).experimental.present).toBe(false); + // experimentalConfigToJson's per-key null-skip is unreachable via parseFocusManifest today (a single-key + // EXPERIMENTAL_PLUGIN_KEYS means `present: true` always implies that one key is non-null) — but the + // function is exported and pure over the full type, so a directly-constructed config (e.g. a future + // second plugin key left unset while another is set) must still round-trip correctly. + expect(experimentalConfigToJson({ present: true, gittensor: null })).toEqual({}); }); it("parses the contentLane: block (#2435 per-repo registry-lane config), round-trips it, and makes the manifest present", () => {