/** * Guard tests for assistant feature flag conventions: * * 1. Key format: all feature flag keys used in production code must use * simple kebab-case format (e.g., "browser", "contacts"). Any remaining * `skills..enabled` usage outside of migration/backward-compat code is * flagged — including template literal forms like `skills.${skillId}.enabled`. * * 2. Declaration coverage: all literal keys passed to * `isAssistantFeatureFlagEnabled('', ...)` in production code must be * declared in the unified registry. This keeps flag usage declarative while * allowing skills to exist without corresponding feature flags. */ import { execSync } from "node:child_process"; import { readFileSync } from "node:fs"; import { join } from "node:path"; import { describe, expect, test } from "bun:test"; // --------------------------------------------------------------------------- // Helpers // --------------------------------------------------------------------------- /** Resolve the repo root from the assistant/ package directory. */ function getRepoRoot(): string { return join(process.cwd(), ".."); } interface RegistryFlag { id: string; scope: string; key: string; label: string; description: string; defaultEnabled: boolean; } interface Registry { version: number; flags: RegistryFlag[]; } function loadRegistry(): Registry { const registryPath = join( getRepoRoot(), "meta", "feature-flags", "feature-flag-registry.json", ); return JSON.parse(readFileSync(registryPath, "utf-8")); } /** * Files allowed to contain `skills..enabled` string literals because they * are part of the backward-compat / migration layer or are test files * exercising legacy paths. */ const LEGACY_KEY_ALLOWLIST = new Set([ // Type definitions documenting the legacy format "assistant/src/config/types.ts", ]); function isTestFile(filePath: string): boolean { return ( filePath.includes("/__tests__/") || filePath.endsWith(".test.ts") || filePath.endsWith(".test.js") || filePath.endsWith(".spec.ts") || filePath.endsWith(".spec.js") ); } // --------------------------------------------------------------------------- // Guard 1: Key format — no stale `skills..enabled` in production code // --------------------------------------------------------------------------- describe("assistant feature flag key format guard", () => { test("no production TypeScript files use skills..enabled outside allowlist", () => { const repoRoot = getRepoRoot(); // Search for string literals and template literals containing // `skills..enabled` or `skills.${...}.enabled` in .ts files // under assistant/src/ and gateway/src/ (excluding test files and // allowlisted paths). The pattern catches both literal keys // (e.g., `skills.foo.enabled`) and template literal forms // (e.g., `skills.${skillId}.enabled`). let grepOutput = ""; try { grepOutput = execSync( `git grep -lE "skills\\.[a-z0-9_-]+\\.enabled|skills\\.\\$\\{" -- 'assistant/src/**/*.ts' 'gateway/src/**/*.ts'`, { encoding: "utf-8", cwd: repoRoot }, ).trim(); } catch (err) { // Exit code 1 means no matches — happy path if ((err as { status?: number }).status === 1) { return; } throw err; } const files = grepOutput.split("\n").filter((f) => f.length > 0); const violations = files.filter((f) => { if (isTestFile(f)) { return false; } if (LEGACY_KEY_ALLOWLIST.has(f)) { return false; } return true; }); if (violations.length > 0) { const message = [ "Found production TypeScript files using legacy `skills..enabled` key format.", "Use simple kebab-case keys instead (e.g., `contacts`, `browser`).", "Call `isAssistantFeatureFlagEnabled(skillId, config)` to check skill flags.", "", "Violations:", ...violations.map((f) => ` - ${f}`), "", "If this is a legitimate backward-compat path, add it to LEGACY_KEY_ALLOWLIST in", "assistant-feature-flag-guardrails.test.ts.", ].join("\n"); expect(violations, message).toEqual([]); } }); }); // --------------------------------------------------------------------------- // Guard 2: Declaration coverage for literal key usage // --------------------------------------------------------------------------- describe("assistant feature flag declaration coverage guard", () => { test("all literal flag keys in isAssistantFeatureFlagEnabled calls are declared in the unified registry", () => { const repoRoot = getRepoRoot(); // Load the unified registry and extract assistant-scope keys const registry = loadRegistry(); const declaredKeys = new Set( registry.flags.filter((f) => f.scope === "assistant").map((f) => f.key), ); // Extract full keys from isAssistantFeatureFlagEnabled('', ...) calls // in non-test production files. We read each matching file and apply a // multiline regex so that calls split across lines are still caught: // // isAssistantFeatureFlagEnabled( // 'browser', // config, // ) // const usedKeys = new Set(); let matchingFiles = ""; try { matchingFiles = execSync( `git grep -l "isAssistantFeatureFlagEnabled" -- 'assistant/src/**/*.ts' ':!assistant/src/__tests__/**'`, { encoding: "utf-8", cwd: repoRoot }, ).trim(); } catch (err) { if ((err as { status?: number }).status !== 1) { throw err; } } if (matchingFiles) { // Multiline regex: match the function name, optional whitespace/newlines, // opening paren, optional whitespace/newlines, then a quoted string key. const multilinePattern = /isAssistantFeatureFlagEnabled\(\s*['"]([^'"]+)['"]/g; for (const relPath of matchingFiles.split("\n")) { if (!relPath) { continue; } const absPath = join(repoRoot, relPath); const content = readFileSync(absPath, "utf-8"); for (const match of content.matchAll(multilinePattern)) { usedKeys.add(match[1]); } } } // Check that all used keys are declared in the registry const undeclared: string[] = []; for (const key of usedKeys) { if (!declaredKeys.has(key)) { undeclared.push(key); } } if (undeclared.length > 0) { const message = [ "Found feature flag keys used in production code that are NOT declared in the unified registry.", `Registry: meta/feature-flags/feature-flag-registry.json`, "", "Undeclared keys:", ...undeclared.map((k) => ` - ${k}`), "", 'To fix: add the missing key(s) to the unified registry with scope "assistant".', ].join("\n"); expect(undeclared, message).toEqual([]); } }); });