diff --git a/.github/workflows/generate-toolkit-docs.yml b/.github/workflows/generate-toolkit-docs.yml index 40e0c2d80..4b9d5ea8f 100644 --- a/.github/workflows/generate-toolkit-docs.yml +++ b/.github/workflows/generate-toolkit-docs.yml @@ -48,11 +48,13 @@ jobs: run: pnpm install --frozen-lockfile - name: Generate toolkit docs + # Invoked by path rather than through `pnpm exec`, which would reset the + # working directory to the repo root and break the relative paths below. run: | - pnpm dlx tsx src/cli/index.ts generate \ + ../node_modules/.bin/tsx src/cli/index.ts generate \ --all \ --skip-unchanged \ - --require-complete \ + --preserve-last-known-good \ --verbose \ --api-source tool-metadata \ --tool-metadata-url "$ENGINE_API_URL" \ @@ -81,7 +83,7 @@ jobs: ANTHROPIC_EDITOR_MODEL: ${{ secrets.ANTHROPIC_EDITOR_MODEL || 'claude-sonnet-4-6' }} - name: Sync toolkit sidebar navigation - run: pnpm dlx tsx toolkit-docs-generator/scripts/sync-toolkit-sidebar.ts --remove-empty-sections=false --verbose + run: pnpm exec tsx toolkit-docs-generator/scripts/sync-toolkit-sidebar.ts --remove-empty-sections=false --verbose - name: Create pull request id: cpr @@ -112,6 +114,32 @@ jobs: env: GH_TOKEN: ${{ secrets.DOCS_PUBLISHABLE_GH_TOKEN }} + - name: Warn #proj-docs about preserved or omitted toolkit docs + continue-on-error: true + run: | + report=toolkit-docs-generator-verification/logs/failed-tools.json + if [ ! -f "$report" ]; then + exit 0 + fi + + preserved=$(jq -r '(.preservedToolkits // []) | join(", ")' "$report") + omitted=$(jq -r '(.omittedToolkits // []) | join(", ")' "$report") + if [ -z "$preserved" ] && [ -z "$omitted" ]; then + exit 0 + fi + + payload=$(jq -n \ + --arg run_url "${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}" \ + --arg preserved "$preserved" \ + --arg omitted "$omitted" \ + '{text: (":warning: Toolkit docs generation completed with recoverable failures\n\n*Workflow run:* <" + $run_url + "|Open run>" + (if $preserved != "" then "\n*Continuing to serve previous docs:* " + $preserved else "" end) + (if $omitted != "" then "\n*No docs are being served (no prior output):* " + $omitted else "" end))}') + + curl --fail-with-body --silent --show-error \ + -X POST \ + -H "Content-Type: application/json" \ + --data "$payload" \ + "${{ secrets.SLACK_PROJ_DOCS_WEBHOOK_URL }}" + alert: name: Alert on generation failure if: ${{ always() && needs.generate.result == 'failure' }} diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 8957e46d9..147e17586 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -41,6 +41,9 @@ jobs: - name: Run linter run: pnpm run lint + - name: Run typecheck + run: pnpm run typecheck + - name: Try a build run: pnpm build diff --git a/.github/workflows/translate-docs.yml b/.github/workflows/translate-docs.yml index 42d545b02..9741fc949 100644 --- a/.github/workflows/translate-docs.yml +++ b/.github/workflows/translate-docs.yml @@ -72,7 +72,7 @@ jobs: if: inputs.cleanup_deleted run: | echo "🔍 Checking for deleted English files..." - pnpm dlx tsx scripts/i18n-sync/index.ts --cleanup + pnpm exec tsx scripts/i18n-sync/index.ts --cleanup env: OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} @@ -81,7 +81,7 @@ jobs: echo "🚀 Starting translation process..." # Build the command with dynamic inputs - CMD="pnpm dlx tsx scripts/i18n-sync/index.ts" + CMD="pnpm exec tsx scripts/i18n-sync/index.ts" # Add target locale if not 'all' if [ "${{ inputs.target_locale }}" != "all" ]; then diff --git a/.husky/pre-commit b/.husky/pre-commit index 452307eb9..a55f31a75 100755 --- a/.husky/pre-commit +++ b/.husky/pre-commit @@ -2,16 +2,6 @@ # Exit on any error set -e -# Detect merge/rebase state — used later to skip the stash+format block -# which conflicts with merge state and corrupts it. -GIT_DIR="$(git rev-parse --git-dir)" -IS_MERGING=false -if [ -f "$GIT_DIR/MERGE_HEAD" ] || \ - [ -d "$GIT_DIR/rebase-merge" ] || \ - [ -d "$GIT_DIR/rebase-apply" ]; then - IS_MERGING=true -fi - # Check if there are any staged files if [ -z "$(git diff --cached --name-only)" ]; then echo "No staged files to check" @@ -133,90 +123,3 @@ fi # --- Lint Staged (formatting) --- pnpm exec lint-staged - -# --- Stash + Format --- -# Skip this block during merge/rebase: git stash --keep-index destroys -# MERGE_HEAD and corrupts the merge state, causing repeated failures. -# lint-staged (above) already handles formatting for staged files safely. -if [ "$IS_MERGING" = true ]; then - echo "⏭️ Skipping stash+format (merge/rebase in progress)" - exit 0 -fi - -# Store the hash of staged changes to detect modifications -STAGED_HASH=$(git diff --cached | sha256sum | cut -d' ' -f1) - -# Save list of staged files (handling all file states) -STAGED_FILES=$(git diff --cached --name-only --diff-filter=ACMR) -PARTIALLY_STAGED=$(git diff --name-only) - -# If a file is both staged and unstaged, stash/pop can produce conflicts. -# In that case rely on lint-staged only, which already ran above. -if [ -n "$PARTIALLY_STAGED" ] && [ -n "$STAGED_FILES" ]; then - for file in $PARTIALLY_STAGED; do - if [ -f "$file" ] && echo "$STAGED_FILES" | grep -qxF "$file"; then - echo "⏭️ Skipping stash+format (partially staged files detected)" - exit 0 - fi - done -fi - -# Stash unstaged changes to preserve working directory -# --keep-index keeps staged changes in working tree -STASH_CREATED=false -STASH_MESSAGE="pre-commit-stash-$$-$(date +%s)" -if ! git diff --quiet; then - git stash push --quiet --keep-index --message "$STASH_MESSAGE" - TOP_STASH_SUBJECT="$(git stash list -1 --format='%s' || true)" - case "$TOP_STASH_SUBJECT" in - *"$STASH_MESSAGE") - STASH_CREATED=true - ;; - esac -fi - -# Run formatter on the staged files -if [ -n "$STAGED_FILES" ]; then - for file in $STAGED_FILES; do - if [ -f "$file" ]; then - pnpm exec ultracite fix "$file" - fi - done -fi -FORMAT_EXIT_CODE=0 - -# Restore working directory state -if [ "$STASH_CREATED" = true ]; then - # Re-stage the formatted files - if [ -n "$STAGED_FILES" ]; then - echo "$STAGED_FILES" | while IFS= read -r file; do - if [ -f "$file" ]; then - git add "$file" - fi - done - fi - - # Restore unstaged changes - if ! git stash pop --quiet; then - echo "❌ Failed to restore stashed changes during pre-commit." - echo " Resolve conflicts, then re-stage files and commit again." - exit 1 - fi -else - # No stash was created, just re-add the formatted files - if [ -n "$STAGED_FILES" ]; then - echo "$STAGED_FILES" | while IFS= read -r file; do - if [ -f "$file" ]; then - git add "$file" - fi - done - fi -fi - -# Check if staged files actually changed -NEW_STAGED_HASH=$(git diff --cached | sha256sum | cut -d' ' -f1) -if [ "$STAGED_HASH" != "$NEW_STAGED_HASH" ]; then - echo "✨ Files formatted by Ultracite" -fi - -exit $FORMAT_EXIT_CODE diff --git a/package.json b/package.json index 657540f93..6dac3dfc7 100644 --- a/package.json +++ b/package.json @@ -9,22 +9,23 @@ "start": "next start", "lint": "pnpm exec ultracite check", "format": "pnpm exec ultracite fix", + "typecheck": "tsc -p tsconfig.json --noEmit && tsc -p scripts/tsconfig.json --noEmit && tsc -p toolkit-docs-generator/tsconfig.json --noEmit", "prepare": "husky install", - "translate": "pnpm dlx tsx scripts/i18n-sync/index.ts && pnpm format", - "llmstxt": "pnpm dlx tsx scripts/generate-llmstxt.ts", + "translate": "pnpm exec tsx scripts/i18n-sync/index.ts && pnpm format", + "llmstxt": "pnpm exec tsx scripts/generate-llmstxt.ts", "test": "vitest --run", "test:watch": "vitest --watch", "vale": "vale", "vale:check": "vale app/en/", - "vale:fix": "pnpm dlx tsx scripts/vale-fix.ts", - "vale:review": "pnpm dlx tsx scripts/vale-style-review.ts", - "vale:editorial": "pnpm dlx tsx scripts/vale-editorial.ts", + "vale:fix": "pnpm exec tsx scripts/vale-fix.ts", + "vale:review": "pnpm exec tsx scripts/vale-style-review.ts", + "vale:editorial": "pnpm exec tsx scripts/vale-editorial.ts", "vale:sync": "vale sync", - "check-redirects": "pnpm dlx tsx scripts/check-redirects.ts", - "update-links": "pnpm dlx tsx scripts/update-internal-links.ts", - "check-meta": "pnpm dlx tsx scripts/check-meta-keys.ts", - "sync-crawler-config": "pnpm dlx tsx scripts/sync-crawler-config.ts", - "metadata-report": "pnpm dlx tsx toolkit-docs-generator/scripts/report-tool-metadata.ts" + "check-redirects": "pnpm exec tsx scripts/check-redirects.ts", + "update-links": "pnpm exec tsx scripts/update-internal-links.ts", + "check-meta": "pnpm exec tsx scripts/check-meta-keys.ts", + "sync-crawler-config": "pnpm exec tsx scripts/sync-crawler-config.ts", + "metadata-report": "pnpm exec tsx toolkit-docs-generator/scripts/report-tool-metadata.ts" }, "repository": { "type": "git", @@ -91,6 +92,7 @@ "ora": "9.4.0", "postcss": "8.5.15", "tailwindcss": "4.3.0", + "tsx": "^4.23.8", "typescript": "5.9.3", "ultracite": "6.1.0", "vite": "7.3.5", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 761fc4215..7ab544f7d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -164,6 +164,9 @@ importers: tailwindcss: specifier: 4.3.0 version: 4.3.0 + tsx: + specifier: ^4.23.8 + version: 4.23.8 typescript: specifier: 5.9.3 version: 5.9.3 @@ -172,10 +175,10 @@ importers: version: 6.1.0(typescript@5.9.3) vite: specifier: 7.3.5 - version: 7.3.5(@types/node@22.19.17)(jiti@2.6.1)(lightningcss@1.32.0)(yaml@2.8.3) + version: 7.3.5(@types/node@22.19.17)(jiti@2.6.1)(lightningcss@1.32.0)(tsx@4.23.8)(yaml@2.8.3) vitest: specifier: 4.1.8 - version: 4.1.8(@opentelemetry/api@1.9.0)(@types/node@22.19.17)(vite@7.3.5(@types/node@22.19.17)(jiti@2.6.1)(lightningcss@1.32.0)(yaml@2.8.3)) + version: 4.1.8(@opentelemetry/api@1.9.0)(@types/node@22.19.17)(vite@7.3.5(@types/node@22.19.17)(jiti@2.6.1)(lightningcss@1.32.0)(tsx@4.23.8)(yaml@2.8.3)) packages: @@ -4374,6 +4377,11 @@ packages: tslib@2.8.1: resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + tsx@4.23.8: + resolution: {integrity: sha512-8W675THjbzfFmLOQzjDBIBna+WjqMGIxmSZ1mMc1+o9qoVsEuAgQu5j5ueLhau8inOkDu9OslVg0FmfBs1RIHw==} + engines: {node: '>=18.0.0'} + hasBin: true + twoslash-protocol@0.3.4: resolution: {integrity: sha512-HHd7lzZNLUvjPzG/IE6js502gEzLC1x7HaO1up/f72d8G8ScWAs9Yfa97igelQRDl5h9tGcdFsRp+lNVre1EeQ==} @@ -6729,13 +6737,13 @@ snapshots: chai: 6.2.2 tinyrainbow: 3.1.0 - '@vitest/mocker@4.1.8(vite@7.3.5(@types/node@22.19.17)(jiti@2.6.1)(lightningcss@1.32.0)(yaml@2.8.3))': + '@vitest/mocker@4.1.8(vite@7.3.5(@types/node@22.19.17)(jiti@2.6.1)(lightningcss@1.32.0)(tsx@4.23.8)(yaml@2.8.3))': dependencies: '@vitest/spy': 4.1.8 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: - vite: 7.3.5(@types/node@22.19.17)(jiti@2.6.1)(lightningcss@1.32.0)(yaml@2.8.3) + vite: 7.3.5(@types/node@22.19.17)(jiti@2.6.1)(lightningcss@1.32.0)(tsx@4.23.8)(yaml@2.8.3) '@vitest/pretty-format@4.1.8': dependencies: @@ -9708,6 +9716,12 @@ snapshots: tslib@2.8.1: {} + tsx@4.23.8: + dependencies: + esbuild: 0.28.1 + optionalDependencies: + fsevents: 2.3.3 + twoslash-protocol@0.3.4: {} twoslash@0.3.4(typescript@5.9.3): @@ -9902,7 +9916,7 @@ snapshots: d3-time: 3.1.0 d3-timer: 3.0.1 - vite@7.3.5(@types/node@22.19.17)(jiti@2.6.1)(lightningcss@1.32.0)(yaml@2.8.3): + vite@7.3.5(@types/node@22.19.17)(jiti@2.6.1)(lightningcss@1.32.0)(tsx@4.23.8)(yaml@2.8.3): dependencies: esbuild: 0.28.1 fdir: 6.5.0(picomatch@4.0.5) @@ -9915,12 +9929,13 @@ snapshots: fsevents: 2.3.3 jiti: 2.6.1 lightningcss: 1.32.0 + tsx: 4.23.8 yaml: 2.8.3 - vitest@4.1.8(@opentelemetry/api@1.9.0)(@types/node@22.19.17)(vite@7.3.5(@types/node@22.19.17)(jiti@2.6.1)(lightningcss@1.32.0)(yaml@2.8.3)): + vitest@4.1.8(@opentelemetry/api@1.9.0)(@types/node@22.19.17)(vite@7.3.5(@types/node@22.19.17)(jiti@2.6.1)(lightningcss@1.32.0)(tsx@4.23.8)(yaml@2.8.3)): dependencies: '@vitest/expect': 4.1.8 - '@vitest/mocker': 4.1.8(vite@7.3.5(@types/node@22.19.17)(jiti@2.6.1)(lightningcss@1.32.0)(yaml@2.8.3)) + '@vitest/mocker': 4.1.8(vite@7.3.5(@types/node@22.19.17)(jiti@2.6.1)(lightningcss@1.32.0)(tsx@4.23.8)(yaml@2.8.3)) '@vitest/pretty-format': 4.1.8 '@vitest/runner': 4.1.8 '@vitest/snapshot': 4.1.8 @@ -9937,7 +9952,7 @@ snapshots: tinyexec: 1.1.1 tinyglobby: 0.2.17 tinyrainbow: 3.1.0 - vite: 7.3.5(@types/node@22.19.17)(jiti@2.6.1)(lightningcss@1.32.0)(yaml@2.8.3) + vite: 7.3.5(@types/node@22.19.17)(jiti@2.6.1)(lightningcss@1.32.0)(tsx@4.23.8)(yaml@2.8.3) why-is-node-running: 2.3.0 optionalDependencies: '@opentelemetry/api': 1.9.0 diff --git a/toolkit-docs-generator/tests/app-lib/available-tools-filter-behavior.test.ts b/tests/available-tools-filter-behavior.test.ts similarity index 94% rename from toolkit-docs-generator/tests/app-lib/available-tools-filter-behavior.test.ts rename to tests/available-tools-filter-behavior.test.ts index 7c1c2e24a..87fc97e13 100644 --- a/toolkit-docs-generator/tests/app-lib/available-tools-filter-behavior.test.ts +++ b/tests/available-tools-filter-behavior.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from "vitest"; -import { filterTools } from "../../../app/_components/toolkit-docs/components/available-tools-filter"; +import { filterTools } from "@/app/_components/toolkit-docs/components/available-tools-filter"; const makeTool = ( name: string, diff --git a/toolkit-docs-generator/tests/app-lib/available-tools-filter-operations.test.ts b/tests/available-tools-filter-operations.test.ts similarity index 94% rename from toolkit-docs-generator/tests/app-lib/available-tools-filter-operations.test.ts rename to tests/available-tools-filter-operations.test.ts index 223fc6fc6..56c9796aa 100644 --- a/toolkit-docs-generator/tests/app-lib/available-tools-filter-operations.test.ts +++ b/tests/available-tools-filter-operations.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from "vitest"; -import { filterTools } from "../../../app/_components/toolkit-docs/components/available-tools-filter"; +import { filterTools } from "@/app/_components/toolkit-docs/components/available-tools-filter"; const makeTool = (name: string, operations: string[]) => ({ name, diff --git a/toolkit-docs-generator/tests/app-lib/shared-service-domain.test.ts b/tests/shared-service-domain.test.ts similarity index 93% rename from toolkit-docs-generator/tests/app-lib/shared-service-domain.test.ts rename to tests/shared-service-domain.test.ts index f3b2eecd3..49e2d8b41 100644 --- a/toolkit-docs-generator/tests/app-lib/shared-service-domain.test.ts +++ b/tests/shared-service-domain.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from "vitest"; -import { getSharedServiceDomain } from "../../../app/_components/toolkit-docs/components/toolkit-page-utils"; +import { getSharedServiceDomain } from "@/app/_components/toolkit-docs/components/toolkit-page-utils"; const makeTool = (domains: string[]) => ({ metadata: { diff --git a/toolkit-docs-generator/tests/app-lib/toolkit-data.test.ts b/tests/toolkit-data.test.ts similarity index 95% rename from toolkit-docs-generator/tests/app-lib/toolkit-data.test.ts rename to tests/toolkit-data.test.ts index af99af5e1..84decef5e 100644 --- a/toolkit-docs-generator/tests/app-lib/toolkit-data.test.ts +++ b/tests/toolkit-data.test.ts @@ -3,13 +3,13 @@ import { tmpdir } from "node:os"; import { basename, join } from "node:path"; import { describe, expect, it } from "vitest"; -import { - readToolkitData, - readToolkitIndex, -} from "../../../app/_lib/toolkit-data"; +import { readToolkitData, readToolkitIndex } from "@/app/_lib/toolkit-data"; const loadFixture = async (fileName: string): Promise => { - const fixturesDir = new URL("../fixtures/", import.meta.url); + const fixturesDir = new URL( + "../toolkit-docs-generator/tests/fixtures/", + import.meta.url + ); const filePath = new URL(fileName, fixturesDir); return await readFile(filePath, "utf-8"); }; diff --git a/tests/toolkit-markdown.test.ts b/tests/toolkit-markdown.test.ts index 456358329..e2a166be1 100644 --- a/tests/toolkit-markdown.test.ts +++ b/tests/toolkit-markdown.test.ts @@ -20,8 +20,11 @@ const fixture: ToolkitData = { isPro: false, type: "arcade", docsLink: "", + isComingSoon: false, + isHidden: false, }, auth: null, + documentationChunks: [], customImports: [], subPages: [], tools: [ @@ -37,6 +40,7 @@ const fixture: ToolkitData = { required: true, description: "Who to do the thing for", enum: null, + inferrable: true, }, ], auth: { providerId: "demo", providerType: "oauth2", scopes: ["scope.a"] }, diff --git a/toolkit-docs-generator/tests/app-lib/toolkit-slug.test.ts b/tests/toolkit-slug.test.ts similarity index 98% rename from toolkit-docs-generator/tests/app-lib/toolkit-slug.test.ts rename to tests/toolkit-slug.test.ts index b18317bf8..faee2b6a8 100644 --- a/toolkit-docs-generator/tests/app-lib/toolkit-slug.test.ts +++ b/tests/toolkit-slug.test.ts @@ -4,7 +4,7 @@ import { normalizeToolkitId, type ToolkitSlugSource, toKebabCase, -} from "../../src/shared/toolkit-primitives"; +} from "@/toolkit-docs-generator/src/shared/toolkit-primitives"; // ============================================================================ // normalizeToolkitId diff --git a/toolkit-docs-generator/tests/app-lib/toolkit-static-params.test.ts b/tests/toolkit-static-params.test.ts similarity index 96% rename from toolkit-docs-generator/tests/app-lib/toolkit-static-params.test.ts rename to tests/toolkit-static-params.test.ts index 040e49c34..6b8e92b0a 100644 --- a/toolkit-docs-generator/tests/app-lib/toolkit-static-params.test.ts +++ b/tests/toolkit-static-params.test.ts @@ -6,8 +6,10 @@ import { getToolkitStaticParamsForCategory, listToolkitRoutes, type ToolkitCatalogEntry, -} from "../../../app/_lib/toolkit-static-params"; -import { normalizeToolkitId } from "../../src/shared/toolkit-primitives"; +} from "@/app/_lib/toolkit-static-params"; +import { normalizeToolkitId } from "@/toolkit-docs-generator/src/shared/toolkit-primitives"; + +const INVALID_TOOLKIT_INDEX_SCHEMA_ERROR = /Invalid toolkit index schema/; const withTempDir = async (fn: (dir: string) => Promise) => { const dir = await mkdtemp(join(tmpdir(), "toolkit-static-params-")); @@ -263,7 +265,7 @@ describe("toolkit static params", () => { await expect( listToolkitRoutes({ dataDir: dir, toolkitsCatalog: [] }) - ).rejects.toThrow(/Invalid toolkit index schema/); + ).rejects.toThrow(INVALID_TOOLKIT_INDEX_SCHEMA_ERROR); }); }); diff --git a/toolkit-docs-generator/scripts/check-stale-summaries.ts b/toolkit-docs-generator/scripts/check-stale-summaries.ts index 73930978e..65a8f09c1 100644 --- a/toolkit-docs-generator/scripts/check-stale-summaries.ts +++ b/toolkit-docs-generator/scripts/check-stale-summaries.ts @@ -14,7 +14,7 @@ import { readdirSync, readFileSync } from "node:fs"; import { join } from "node:path"; -import { resolveToolkitDataDir } from "../src/shared/toolkit-data-dir.ts"; +import { resolveToolkitDataDir } from "../src/shared/toolkit-data-dir"; const TOOLKITS_DIR = resolveToolkitDataDir(); diff --git a/toolkit-docs-generator/scripts/merge-custom-sections.ts b/toolkit-docs-generator/scripts/merge-custom-sections.ts index bc0298fbf..4acbbf85b 100644 --- a/toolkit-docs-generator/scripts/merge-custom-sections.ts +++ b/toolkit-docs-generator/scripts/merge-custom-sections.ts @@ -8,7 +8,15 @@ */ import { readdir, readFile, writeFile } from "fs/promises"; -import { join } from "path"; +import { dirname, join } from "path"; +import { fileURLToPath } from "url"; + +/** + * Anchored on this file rather than the working directory, so the defaults + * below mean the same thing from the repo root and from + * toolkit-docs-generator/. Both are still overridable by flag. + */ +const GENERATOR_ROOT = join(dirname(fileURLToPath(import.meta.url)), ".."); const JSON_PRETTY_PRINT_INDENT = 2; const ERROR_PREVIEW_LIMIT = 10; @@ -54,18 +62,25 @@ type MergeOutcome = { }; const parseArgs = (args: string[]): MergeOptions => { - let customSectionsPath = "../data/custom_sections_for_merge.json"; - let toolkitsDir = "data/toolkits"; + let customSectionsPath = join( + GENERATOR_ROOT, + "..", + "data", + "custom_sections_for_merge.json" + ); + let toolkitsDir = join(GENERATOR_ROOT, "data", "toolkits"); let verbose = false; for (let i = 0; i < args.length; i++) { - if (args[i] === "--custom-sections" && args[i + 1]) { - customSectionsPath = args[i + 1]; + const arg = args[i]; + const next = args[i + 1]; + if (arg === "--custom-sections" && next) { + customSectionsPath = next; i++; - } else if (args[i] === "--toolkits-dir" && args[i + 1]) { - toolkitsDir = args[i + 1]; + } else if (arg === "--toolkits-dir" && next) { + toolkitsDir = next; i++; - } else if (args[i] === "--verbose" || args[i] === "-v") { + } else if (arg === "--verbose" || arg === "-v") { verbose = true; } } diff --git a/toolkit-docs-generator/scripts/report-tool-metadata.ts b/toolkit-docs-generator/scripts/report-tool-metadata.ts index 428317a2a..a16ed03f5 100644 --- a/toolkit-docs-generator/scripts/report-tool-metadata.ts +++ b/toolkit-docs-generator/scripts/report-tool-metadata.ts @@ -5,8 +5,8 @@ * regardless of cwd and honors the TOOLKIT_DATA_DIR env var override. */ -import { resolveToolkitDataDir } from "../src/shared/toolkit-data-dir.ts"; -import { collectToolMetadataStats } from "../src/utils/tool-metadata-audit.ts"; +import { resolveToolkitDataDir } from "../src/shared/toolkit-data-dir"; +import { collectToolMetadataStats } from "../src/utils/tool-metadata-audit"; const DATA_DIR = resolveToolkitDataDir(); diff --git a/toolkit-docs-generator/scripts/sync-toolkit-sidebar.ts b/toolkit-docs-generator/scripts/sync-toolkit-sidebar.ts index 398ab51ed..8b753dd86 100644 --- a/toolkit-docs-generator/scripts/sync-toolkit-sidebar.ts +++ b/toolkit-docs-generator/scripts/sync-toolkit-sidebar.ts @@ -28,16 +28,16 @@ import { import { dirname, join, resolve } from "node:path"; import { fileURLToPath } from "node:url"; import { TOOLKITS as DESIGN_SYSTEM_TOOLKITS } from "@arcadeai/design-system/metadata/toolkits"; -import { resolveToolkitDataDir } from "../src/shared/toolkit-data-dir.ts"; +import { resolveToolkitDataDir } from "../src/shared/toolkit-data-dir"; import { getToolkitSlug, INTEGRATION_CATEGORIES, isApiSuffixedToolkitId, -} from "../src/shared/toolkit-primitives.ts"; +} from "../src/shared/toolkit-primitives"; import type { MergedToolkit, MergedToolkitMetadata, -} from "../src/shared/toolkit-schemas.ts"; +} from "../src/shared/toolkit-schemas"; const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); @@ -271,7 +271,7 @@ function resolveToolkitInfo( const toolkitId = jsonData?.id ?? slug; const docsSlug = getToolkitSlug({ id: toolkitId, - docsLink: jsonData?.metadata?.docsLink, + docsLink: jsonData?.metadata?.docsLink ?? null, }); const designSystemToolkit = TOOLKITS.find( (t) => t.id.toLowerCase() === toolkitId.toLowerCase() diff --git a/toolkit-docs-generator/scripts/validate-merge.ts b/toolkit-docs-generator/scripts/validate-merge.ts index 89d4ed2ed..c2fd37690 100644 --- a/toolkit-docs-generator/scripts/validate-merge.ts +++ b/toolkit-docs-generator/scripts/validate-merge.ts @@ -13,8 +13,8 @@ import { existsSync, readdirSync, readFileSync } from "node:fs"; import { join, resolve } from "node:path"; import { fileURLToPath } from "node:url"; -import { resolveToolkitDataDir } from "../src/shared/toolkit-data-dir.ts"; -import type { MergedToolkit } from "../src/shared/toolkit-schemas.ts"; +import { resolveToolkitDataDir } from "../src/shared/toolkit-data-dir"; +import type { MergedToolkit } from "../src/shared/toolkit-schemas"; const DATA_DIR = resolveToolkitDataDir(); diff --git a/toolkit-docs-generator/src/cli/exclusion-cleanup.ts b/toolkit-docs-generator/src/cli/exclusion-cleanup.ts index 3426f9798..a4e805299 100644 --- a/toolkit-docs-generator/src/cli/exclusion-cleanup.ts +++ b/toolkit-docs-generator/src/cli/exclusion-cleanup.ts @@ -1,6 +1,6 @@ import { resolve } from "path"; -import type { RebuildIndexResult } from "../generator/json-generator.js"; -import { removeExcludedToolkitFiles } from "../utils/excluded-output-cleanup.js"; +import type { RebuildIndexResult } from "../generator/json-generator"; +import { removeExcludedToolkitFiles } from "../utils/excluded-output-cleanup"; type RebuildIndexGenerator = { rebuildIndexFromOutput: () => Promise; diff --git a/toolkit-docs-generator/src/cli/generate-flow.ts b/toolkit-docs-generator/src/cli/generate-flow.ts index 194634e19..0e046f618 100644 --- a/toolkit-docs-generator/src/cli/generate-flow.ts +++ b/toolkit-docs-generator/src/cli/generate-flow.ts @@ -1,4 +1,4 @@ -import type { ChangeDetectionResult } from "../diff/index.js"; +import type { ChangeDetectionResult } from "../diff/index"; /** * Extract the lowercase toolkit IDs that were removed (present in previous diff --git a/toolkit-docs-generator/src/cli/index.ts b/toolkit-docs-generator/src/cli/index.ts index 69e2c8b0c..9bb540de7 100644 --- a/toolkit-docs-generator/src/cli/index.ts +++ b/toolkit-docs-generator/src/cli/index.ts @@ -22,13 +22,13 @@ import { formatDetailedChanges, getChangedToolkitIds, hasChanges, -} from "../diff/index.js"; -import { parsePreviousToolkitForDiff } from "../diff/previous-output.js"; +} from "../diff/index"; +import { parsePreviousToolkitForDiff } from "../diff/previous-output"; import { createJsonGenerator, type VerificationProgress, verifyOutputDir, -} from "../generator/index.js"; +} from "../generator/index"; import { createLlmClient, type LlmClient, @@ -36,14 +36,17 @@ import { LlmSecretEditGenerator, LlmToolExampleGenerator, LlmToolkitSummaryGenerator, -} from "../llm/index.js"; -import type { MergeResult } from "../merger/data-merger.js"; -import { createDataMerger } from "../merger/data-merger.js"; -import { createCustomSectionsFileSource } from "../sources/custom-sections-file.js"; -import { createDesignSystemMetadataSource } from "../sources/design-system-metadata.js"; -import { createEmptyCustomSectionsSource } from "../sources/in-memory.js"; -import { createMockMetadataSource } from "../sources/mock-metadata.js"; -import { createDesignSystemProviderIdResolver } from "../sources/oauth-provider-resolver.js"; +} from "../llm/index"; +import type { MergeResult } from "../merger/data-merger"; +import { + assertRequireCompleteMetadata, + createDataMerger, +} from "../merger/data-merger"; +import { createCustomSectionsFileSource } from "../sources/custom-sections-file"; +import { createDesignSystemMetadataSource } from "../sources/design-system-metadata"; +import { createEmptyCustomSectionsSource } from "../sources/in-memory"; +import { createMockMetadataSource } from "../sources/mock-metadata"; +import { createDesignSystemProviderIdResolver } from "../sources/oauth-provider-resolver"; import { createArcadeToolkitDataSource, createCachedToolkitDataSource, @@ -51,36 +54,36 @@ import { createMockToolkitDataSource, type IToolkitDataSource, type ToolkitData, -} from "../sources/toolkit-data-source.js"; +} from "../sources/toolkit-data-source"; import { type MergedToolkit, type ProviderVersion, ProviderVersionSchema, -} from "../types/index.js"; -import { readExclusionList } from "../utils/exclusion-list.js"; -import { readIgnoreList } from "../utils/ignore-list.js"; +} from "../types/index"; +import { readExclusionList } from "../utils/exclusion-list"; +import { readIgnoreList } from "../utils/ignore-list"; import { clearSafeOutputDir, resolveDefaultOutputDir, -} from "../utils/output-dir.js"; +} from "../utils/output-dir"; import { createProgressTracker, formatToolkitComplete, -} from "../utils/progress.js"; -import { resolveProviderIdsFromMetadata } from "../utils/provider-matching.js"; +} from "../utils/progress"; +import { resolveProviderIdsFromMetadata } from "../utils/provider-matching"; import { appendLogEntry, readFailedToolsReport, writeFailedToolsReport, -} from "../utils/run-logs.js"; -import { type ApiSource, resolveApiSource } from "./api-source.js"; -import { cleanupExcludedToolkitOutput } from "./exclusion-cleanup.js"; +} from "../utils/run-logs"; +import { type ApiSource, resolveApiSource } from "./api-source"; +import { cleanupExcludedToolkitOutput } from "./exclusion-cleanup"; import { assertSafeCurrentToolkitSnapshot, collectRemovedToolkitIds, computeProcessingStats, filterProvidersBySkipIds, -} from "./generate-flow.js"; +} from "./generate-flow"; const program = new Command(); @@ -133,13 +136,6 @@ const buildLogPaths = (logDir: string) => ({ failedToolsPath: join(logDir, "failed-tools.json"), }); -const getToolkitIdsWithoutMetadata = ( - toolkitsData: ReadonlyMap -): string[] => - Array.from(toolkitsData.entries()) - .filter(([, toolkitData]) => toolkitData.metadata === null) - .map(([toolkitId]) => toolkitId); - const createMetadataSource = async (options: { metadataFile: string; useMetadataFile: boolean; @@ -906,6 +902,11 @@ program "Require complete metadata. Only include toolkits with data in Engine and Design System.", false ) + .option( + "--preserve-last-known-good", + "Publish healthy toolkits while preserving prior docs for recoverable toolkit failures", + false + ) .option( "--exclude-file ", "Path to a .txt file with toolkit IDs to skip and delete existing output for (one per line, e.g. remove-toolkits.txt)" @@ -986,6 +987,7 @@ program incremental: boolean; skipUnchanged: boolean; requireComplete: boolean; + preserveLastKnownGood: boolean; excludeFile?: string; ignoreFile?: string; verbose: boolean; @@ -1002,6 +1004,7 @@ program const spinner = ora("Parsing input...").start(); const logPaths = buildLogPaths(resolve(options.logDir)); const requireComplete = options.requireComplete; + const preserveLastKnownGood = options.preserveLastKnownGood; let excludedToolkitIds = new Set(); if (options.excludeFile) { @@ -1303,28 +1306,16 @@ program ); } - const metadataExcludedToolkitIds = requireComplete - ? getToolkitIdsWithoutMetadata(currentToolkitsData) - : []; - const metadataExcludedToolkitIdSet = new Set( - metadataExcludedToolkitIds.map((id) => id.toLowerCase()) - ); - if (options.verbose && metadataExcludedToolkitIds.length > 0) { - console.log( - chalk.dim( - ` Excluding ${metadataExcludedToolkitIds.length} toolkit(s) without metadata before change detection` - ) + if (requireComplete) { + assertRequireCompleteMetadata( + Array.from(currentToolkitsData.entries()) ); } // Build map of toolkit ID -> current toolkit data for comparison - const currentToolkitDataForDiff = new Map(); - for (const [id, data] of currentToolkitsData) { - if (metadataExcludedToolkitIdSet.has(id.toLowerCase())) { - continue; - } - currentToolkitDataForDiff.set(id, data); - } + const currentToolkitDataForDiff = new Map( + currentToolkitsData + ); assertSafeCurrentToolkitSnapshot( currentToolkitDataForDiff.size, previousToolkits?.size ?? 0 @@ -1479,6 +1470,7 @@ program ...(runAll ? { onToolkitProgress } : {}), ...(skipToolkitIds.size > 0 ? { skipToolkitIds } : {}), requireCompleteData: requireComplete, + preserveLastKnownGood, ...(onToolkitComplete ? { onToolkitComplete } : {}), ...(resolveProviderId ? { resolveProviderId } : {}), }); @@ -1501,20 +1493,12 @@ program ); } - if (requireComplete) { - const metadataExcludedToolkitIds = - getToolkitIdsWithoutMetadata(toolkitList); - for (const toolkitId of metadataExcludedToolkitIds) { - skipToolkitIds.add(toolkitId.toLowerCase()); - } - if (options.verbose && metadataExcludedToolkitIds.length > 0) { - console.log( - chalk.dim( - ` Excluding ${metadataExcludedToolkitIds.length} toolkit(s) without metadata` - ) - ); - } - } + // requireComplete no longer silently drops toolkits without + // design-system metadata into skipToolkitIds here. Silently + // excluding them was just as opaque as fabricating metadata for + // them — DataMerger.assertNoMissingMetadata (run from inside + // mergeAllToolkits below) now fails the whole run and names every + // affected toolkit instead. // If --skip-unchanged, only process changed toolkits // Add unchanged toolkits to skipToolkitIds @@ -1611,6 +1595,7 @@ program onToolkitProgress, ...(skipToolkitIds.size > 0 ? { skipToolkitIds } : {}), requireCompleteData: requireComplete, + preserveLastKnownGood, ...(onToolkitComplete ? { onToolkitComplete } : {}), ...(resolveProviderId ? { resolveProviderId } : {}), }); @@ -1661,7 +1646,9 @@ program // Error results can still carry a last-known-good toolkit fallback from // the merger. Keep them in the batch output so one failed merge cannot // silently remove that toolkit from index.json. - const writableResults = allResults; + const writableResults = allResults.filter( + (result) => result.recovery !== "omitted" + ); // Generate output files (batch mode if not incremental) if (!useIncremental && writableResults.length > 0) { @@ -1810,10 +1797,27 @@ program 0 ); const failedToolkits = mergeFailures.map((result) => result.toolkit.id); + const preservedToolkits = mergeFailures + .filter((result) => result.recovery === "preserved") + .map((result) => result.toolkit.id); + const omittedToolkits = mergeFailures + .filter((result) => result.recovery === "omitted") + .map((result) => result.toolkit.id); const failedTools = allResults.flatMap((result) => result.failedTools); const failedToolkitsFromTools = Array.from( new Set(failedTools.map((tool) => tool.toolkitId)) ); + // Toolkits that fell back to getDefaultMetadata's guessed category, + // icon, and docsLink because the design system had no entry for + // them. --require-complete would have already failed the run for + // these (see DataMerger.assertNoMissingMetadata), so reaching here + // means requireComplete was off. The per-toolkit warning text + // already goes to stdout above; naming these explicitly in the run + // log means the omission survives past the CI log window instead + // of only ever being visible in real time. + const toolkitsWithDefaultMetadata = allResults + .filter((result) => result.usedDefaultMetadata) + .map((result) => result.toolkit.id); const runDetails = [ `output=${resolve(options.output)}`, @@ -1821,11 +1825,18 @@ program `mode=${runAll ? "all" : "providers"}`, `skipUnchanged=${options.skipUnchanged}`, `requireComplete=${requireComplete}`, + `preserveLastKnownGood=${preserveLastKnownGood}`, `filesWritten=${filesWritten.length}`, `warnings=${warningCount}`, `writeErrors=${writeErrors.length}`, ]; + if (toolkitsWithDefaultMetadata.length > 0) { + runDetails.push( + `toolkitsWithDefaultMetadata=${toolkitsWithDefaultMetadata.join(", ")}` + ); + } + if (!runAll && providers) { runDetails.push( `providers=${providers.map((p) => p.provider).join(", ")}` @@ -1839,6 +1850,12 @@ program if (failedToolkits.length > 0) { runDetails.push(`failedToolkits=${failedToolkits.join(", ")}`); } + if (preservedToolkits.length > 0) { + runDetails.push(`preservedToolkits=${preservedToolkits.join(", ")}`); + } + if (omittedToolkits.length > 0) { + runDetails.push(`omittedToolkits=${omittedToolkits.join(", ")}`); + } if (failedTools.length > 0) { runDetails.push(`failedTools=${failedTools.length}`); } @@ -1855,6 +1872,8 @@ program generatedAt: new Date().toISOString(), toolkits: failedToolkitsFromTools, failedToolkits, + preservedToolkits, + omittedToolkits, tools: failedTools, }); @@ -1862,7 +1881,10 @@ program title: "generate", details: runDetails, }); - if (mergeFailures.length > 0 || writeErrors.length > 0) { + if ( + writeErrors.length > 0 || + (mergeFailures.length > 0 && !preserveLastKnownGood) + ) { process.exitCode = 1; } } catch (error) { @@ -2276,20 +2298,12 @@ program ); } - if (requireComplete) { - const metadataExcludedToolkitIds = - getToolkitIdsWithoutMetadata(toolkitList); - for (const toolkitId of metadataExcludedToolkitIds) { - skipToolkitIds.add(toolkitId.toLowerCase()); - } - if (options.verbose && metadataExcludedToolkitIds.length > 0) { - console.log( - chalk.dim( - ` Excluding ${metadataExcludedToolkitIds.length} toolkit(s) without metadata` - ) - ); - } - } + // requireComplete no longer silently drops toolkits without + // design-system metadata into skipToolkitIds here. Silently + // excluding them was just as opaque as fabricating metadata for + // them — DataMerger.assertNoMissingMetadata (run from inside + // mergeAllToolkits below) now fails the whole run and names every + // affected toolkit instead. const processingStats = computeProcessingStats( toolkitList, @@ -2547,7 +2561,7 @@ program .description("Validate a generated JSON file against the schema") .action(async (file: string) => { const { readFile } = await import("fs/promises"); - const { MergedToolkitSchema } = await import("../types/index.js"); + const { MergedToolkitSchema } = await import("../types/index"); try { const content = await readFile(file, "utf-8"); diff --git a/toolkit-docs-generator/src/diff/index.ts b/toolkit-docs-generator/src/diff/index.ts index e818d8f84..00627d85c 100644 --- a/toolkit-docs-generator/src/diff/index.ts +++ b/toolkit-docs-generator/src/diff/index.ts @@ -14,7 +14,7 @@ export { type SummaryToolkit, type SummaryToolkitChange, type SummaryToolkitChangeType, -} from "./summary-diff.js"; +} from "./summary-diff"; export { buildToolDefinitionSignature, type ChangeDetectionResult, @@ -30,4 +30,4 @@ export { type ToolChangeType, type ToolkitChange, type ToolkitChangeType, -} from "./toolkit-diff.js"; +} from "./toolkit-diff"; diff --git a/toolkit-docs-generator/src/diff/previous-output.ts b/toolkit-docs-generator/src/diff/previous-output.ts index 043a7bb90..a914a1642 100644 --- a/toolkit-docs-generator/src/diff/previous-output.ts +++ b/toolkit-docs-generator/src/diff/previous-output.ts @@ -5,7 +5,7 @@ import { MergedToolkitSchema, type ToolDefinition, ToolDefinitionSchema, -} from "../types/index.js"; +} from "../types/index"; const DEFAULT_PREVIOUS_TOOLKIT_METADATA = { category: "development" as const, diff --git a/toolkit-docs-generator/src/diff/summary-diff.ts b/toolkit-docs-generator/src/diff/summary-diff.ts index 90e631958..bca43fb1c 100644 --- a/toolkit-docs-generator/src/diff/summary-diff.ts +++ b/toolkit-docs-generator/src/diff/summary-diff.ts @@ -3,8 +3,8 @@ * * Compares tool metadata summaries with previous output to detect version changes. */ -import type { MergedToolkit } from "../types/index.js"; -import { normalizeId } from "../utils/fp.js"; +import type { MergedToolkit } from "../types/index"; +import { normalizeId } from "../utils/fp"; export type SummaryToolkit = { name: string; diff --git a/toolkit-docs-generator/src/diff/toolkit-diff.ts b/toolkit-docs-generator/src/diff/toolkit-diff.ts index d8b04911d..1cd0831d5 100644 --- a/toolkit-docs-generator/src/diff/toolkit-diff.ts +++ b/toolkit-docs-generator/src/diff/toolkit-diff.ts @@ -8,14 +8,14 @@ import { buildComparableToolSignature, stableStringify, -} from "../merger/data-merger.js"; +} from "../merger/data-merger"; import type { MergedTool, MergedToolkit, ToolDefinition, ToolkitMetadata, -} from "../types/index.js"; -import { extractVersion } from "../utils/index.js"; +} from "../types/index"; +import { extractVersion } from "../utils/index"; // ============================================================================ // Types diff --git a/toolkit-docs-generator/src/generator/index.ts b/toolkit-docs-generator/src/generator/index.ts index 7b5edd3c6..742db80d8 100644 --- a/toolkit-docs-generator/src/generator/index.ts +++ b/toolkit-docs-generator/src/generator/index.ts @@ -1,5 +1,5 @@ /** * Generator module exports */ -export * from "./json-generator.js"; -export * from "./output-verifier.js"; +export * from "./json-generator"; +export * from "./output-verifier"; diff --git a/toolkit-docs-generator/src/generator/json-generator.ts b/toolkit-docs-generator/src/generator/json-generator.ts index 1b7ebdec4..9203a1f59 100644 --- a/toolkit-docs-generator/src/generator/json-generator.ts +++ b/toolkit-docs-generator/src/generator/json-generator.ts @@ -6,17 +6,14 @@ import { randomUUID } from "node:crypto"; import { mkdir, readFile, rename, rm, stat, writeFile } from "fs/promises"; import { dirname, join } from "path"; -import { parsePreviousToolkitForDiff } from "../diff/previous-output.js"; +import { parsePreviousToolkitForDiff } from "../diff/previous-output"; import type { MergedToolkit, ToolkitIndex, ToolkitIndexEntry, -} from "../types/index.js"; -import { MergedToolkitSchema } from "../types/index.js"; -import { - readToolkitsFromDir, - type ToolkitReadResult, -} from "./output-verifier.js"; +} from "../types/index"; +import { MergedToolkitSchema } from "../types/index"; +import { readToolkitsFromDir, type ToolkitReadResult } from "./output-verifier"; const SAFE_TOOLKIT_ID = /^[a-z0-9][a-z0-9_-]*$/i; const RESERVED_TOOLKIT_ID = "index"; diff --git a/toolkit-docs-generator/src/generator/output-verifier.ts b/toolkit-docs-generator/src/generator/output-verifier.ts index b28763c17..afcd84e5a 100644 --- a/toolkit-docs-generator/src/generator/output-verifier.ts +++ b/toolkit-docs-generator/src/generator/output-verifier.ts @@ -1,8 +1,8 @@ import { readdir, readFile } from "fs/promises"; import { basename, join } from "path"; -import { parsePreviousToolkitForDiff } from "../diff/previous-output.js"; -import type { MergedToolkit, ToolkitIndex } from "../types/index.js"; -import { MergedToolkitSchema, ToolkitIndexSchema } from "../types/index.js"; +import { parsePreviousToolkitForDiff } from "../diff/previous-output"; +import type { MergedToolkit, ToolkitIndex } from "../types/index"; +import { MergedToolkitSchema, ToolkitIndexSchema } from "../types/index"; export interface OutputVerificationResult { valid: boolean; diff --git a/toolkit-docs-generator/src/index.ts b/toolkit-docs-generator/src/index.ts index 97f39ff57..987f554f6 100644 --- a/toolkit-docs-generator/src/index.ts +++ b/toolkit-docs-generator/src/index.ts @@ -6,14 +6,14 @@ */ // Generator -export * from "./generator/index.js"; +export * from "./generator/index"; // LLM -export * from "./llm/index.js"; +export * from "./llm/index"; // Merger -export * from "./merger/index.js"; +export * from "./merger/index"; // Sources -export * from "./sources/index.js"; +export * from "./sources/index"; // Types -export * from "./types/index.js"; +export * from "./types/index"; // Utils -export * from "./utils/index.js"; +export * from "./utils/index"; diff --git a/toolkit-docs-generator/src/llm/client.ts b/toolkit-docs-generator/src/llm/client.ts index 79933e69d..db75304bf 100644 --- a/toolkit-docs-generator/src/llm/client.ts +++ b/toolkit-docs-generator/src/llm/client.ts @@ -1,6 +1,6 @@ import Anthropic from "@anthropic-ai/sdk"; import OpenAI from "openai"; -import { type RetryOptions, withRetry } from "../utils/retry.js"; +import { type RetryOptions, withRetry } from "../utils/retry"; export type LlmProvider = "openai" | "anthropic"; @@ -145,4 +145,4 @@ export const createLlmClient = (config: LlmClientConfig): LlmClient => { return new AnthropicClient(config.config); }; -export type { RetryOptions } from "../utils/retry.js"; +export type { RetryOptions } from "../utils/retry"; diff --git a/toolkit-docs-generator/src/llm/index.ts b/toolkit-docs-generator/src/llm/index.ts index ef58af37d..9bf040494 100644 --- a/toolkit-docs-generator/src/llm/index.ts +++ b/toolkit-docs-generator/src/llm/index.ts @@ -1,4 +1,4 @@ -export * from "./client.js"; -export * from "./secret-edit-generator.js"; -export * from "./tool-example-generator.js"; -export * from "./toolkit-summary-generator.js"; +export * from "./client"; +export * from "./secret-edit-generator"; +export * from "./tool-example-generator"; +export * from "./toolkit-summary-generator"; diff --git a/toolkit-docs-generator/src/llm/secret-edit-generator.ts b/toolkit-docs-generator/src/llm/secret-edit-generator.ts index ce8bc162e..06c2e52c8 100644 --- a/toolkit-docs-generator/src/llm/secret-edit-generator.ts +++ b/toolkit-docs-generator/src/llm/secret-edit-generator.ts @@ -12,8 +12,8 @@ import { ARCADE_SECRETS_DASHBOARD_URL, ARCADE_SECRETS_DOC_URL, -} from "../merger/secret-coherence.js"; -import type { LlmClient } from "./client.js"; +} from "../merger/secret-coherence"; +import type { LlmClient } from "./client"; export interface SecretEditGeneratorConfig { readonly client: LlmClient; diff --git a/toolkit-docs-generator/src/llm/tool-example-generator.ts b/toolkit-docs-generator/src/llm/tool-example-generator.ts index fe59a7201..dc65db138 100644 --- a/toolkit-docs-generator/src/llm/tool-example-generator.ts +++ b/toolkit-docs-generator/src/llm/tool-example-generator.ts @@ -1,15 +1,15 @@ import type { ToolExampleGenerator, ToolExampleResult, -} from "../merger/data-merger.js"; +} from "../merger/data-merger"; import { type ExampleParameterValue, type SecretType, SecretTypeSchema, type ToolDefinition, type ToolSecret, -} from "../types/index.js"; -import type { LlmClient } from "./client.js"; +} from "../types/index"; +import type { LlmClient } from "./client"; export interface LlmToolExampleGeneratorConfig { readonly client: LlmClient; diff --git a/toolkit-docs-generator/src/llm/toolkit-summary-generator.ts b/toolkit-docs-generator/src/llm/toolkit-summary-generator.ts index 549b742db..f1e7a8462 100644 --- a/toolkit-docs-generator/src/llm/toolkit-summary-generator.ts +++ b/toolkit-docs-generator/src/llm/toolkit-summary-generator.ts @@ -1,11 +1,11 @@ -import type { ToolkitSummaryGenerator } from "../merger/data-merger.js"; +import type { ToolkitSummaryGenerator } from "../merger/data-merger"; import { ARCADE_AUTH_PROVIDERS_BASE_URL, ARCADE_SECRETS_DASHBOARD_URL, ARCADE_SECRETS_DOC_URL, -} from "../merger/secret-coherence.js"; -import type { MergedTool, MergedToolkit, SecretType } from "../types/index.js"; -import type { LlmClient } from "./client.js"; +} from "../merger/secret-coherence"; +import type { MergedTool, MergedToolkit, SecretType } from "../types/index"; +import type { LlmClient } from "./client"; export interface LlmToolkitSummaryGeneratorConfig { readonly client: LlmClient; diff --git a/toolkit-docs-generator/src/merger/data-merger.ts b/toolkit-docs-generator/src/merger/data-merger.ts index 15d28def8..df10931ae 100644 --- a/toolkit-docs-generator/src/merger/data-merger.ts +++ b/toolkit-docs-generator/src/merger/data-merger.ts @@ -5,16 +5,16 @@ * into the final MergedToolkit format. */ -import type { ISecretEditGenerator } from "../llm/secret-edit-generator.js"; +import type { ISecretEditGenerator } from "../llm/secret-edit-generator"; import { isApiSuffixedToolkitId, normalizeToolkitId, -} from "../shared/toolkit-primitives.js"; -import type { ICustomSectionsSource } from "../sources/interfaces.js"; +} from "../shared/toolkit-primitives"; +import type { ICustomSectionsSource } from "../sources/interfaces"; import type { IToolkitDataSource, ToolkitData, -} from "../sources/toolkit-data-source.js"; +} from "../sources/toolkit-data-source"; import type { CustomSections, DocumentationChunk, @@ -26,13 +26,13 @@ import type { ToolDefinition, ToolkitAuthType, ToolkitMetadata, -} from "../types/index.js"; -import { mapWithConcurrency } from "../utils/concurrency.js"; -import { extractVersion } from "../utils/fp.js"; +} from "../types/index"; +import { mapWithConcurrency } from "../utils/concurrency"; +import { extractVersion } from "../utils/fp"; import { detectMetadataChanges, formatFreshnessWarnings, -} from "./metadata-freshness.js"; +} from "./metadata-freshness"; import { collectToolkitSecrets, detectSecretCoherenceIssues, @@ -40,7 +40,7 @@ import { hasCoherenceIssues, type SecretCoherenceIssues, type StaleSecretEditTarget, -} from "./secret-coherence.js"; +} from "./secret-coherence"; // ============================================================================ // Merger Configuration @@ -82,6 +82,8 @@ export interface DataMergerConfig { skipToolkitIds?: ReadonlySet | undefined; /** When true, only process toolkits with metadata and tools */ requireCompleteData?: boolean; + /** Preserve previous output for a broken toolkit instead of failing the run. */ + preserveLastKnownGood?: boolean; /** Fallback resolver: toolkit ID → OAuth provider ID (design system) */ resolveProviderId?: ((toolkitId: string) => string | null) | undefined; } @@ -98,6 +100,17 @@ export interface MergeResult { warnings: string[]; failedTools: FailedTool[]; error?: string; + /** A recoverable failure retained prior output or omitted a new toolkit. */ + recovery?: "preserved" | "omitted"; + /** + * True when the design system had no metadata for this toolkit and + * `getDefaultMetadata`'s placeholder (category, icon, docsLink, and + * `isHidden: true`) was used instead. Also true for the last-known-good + * placeholder in `buildMergeErrorResult`, for the same reason. Callers + * use this to log which toolkits are running on fabricated metadata, + * since that's easy to miss in a warnings list read only on failure. + */ + usedDefaultMetadata: boolean; } export interface ToolExampleResult { @@ -113,6 +126,25 @@ export interface ToolkitSummaryGenerator { generate: (toolkit: MergedToolkit) => Promise; } +/** + * Under `--require-complete`, every toolkit must have design-system metadata. + * Fails the run with every affected toolkit named in one error. + */ +export const assertRequireCompleteMetadata = ( + toolkitEntries: ReadonlyArray +): void => { + const missing = toolkitEntries + .filter(([, toolkitData]) => toolkitData.metadata === null) + .map(([toolkitId]) => toolkitId); + + if (missing.length > 0) { + throw new Error( + `--require-complete: missing design-system metadata for ${missing.length} toolkit(s): ${missing.join(", ")}. ` + + "Add the toolkit to the design system catalog, or drop --require-complete to continue with a hidden placeholder record." + ); + } +}; + interface MergeToolkitOptions { previousToolkit?: MergedToolkit; /** Maximum concurrent LLM calls for tool examples (default: 5) */ @@ -439,16 +471,50 @@ const applyToolkitTypeOverrides = ( return metadata; }; +/** + * Category assigned to a toolkit when the design system has no metadata for + * it at all. `MergedToolkitMetadata.category` is a closed enum + * (`INTEGRATION_CATEGORIES`) with no catch-all value, so this placeholder + * has to be one of the real categories — there is nothing else the schema + * will accept. "development" is picked arbitrarily; it is almost certainly + * wrong for any given toolkit, which is exactly why `getDefaultMetadata` + * also forces `isHidden: true` below instead of trusting this value enough + * to publish a page under it. + */ +const DEFAULT_METADATA_CATEGORY: MergedToolkitMetadata["category"] = + "development"; + +/** + * Metadata used when the design system has no entry for a toolkit at all. + * + * Every field here is a guess, not a fact: none of it came from the design + * system, so none of it should be trusted enough to route or display. The + * category in particular can't be flagged as "unknown" — the schema is a + * closed enum with no catch-all — so a wrong-but-valid category would + * otherwise file the toolkit under the wrong sidebar section with a + * canonical URL nobody chose. `isHidden: true` is what actually neutralizes + * that: `app/_lib/toolkit-static-params.ts` drops hidden toolkits from + * routing entirely, so this placeholder record can exist on disk (and keep + * CI green when metadata truly is optional) without ever rendering under + * the wrong category. Real metadata — pulled in the next successful design + * system sync — clears the flag automatically, since `metadata` will no + * longer be null and this function won't run for that toolkit again. + * + * `DataMerger` only takes this path when `requireCompleteData` is false; + * under `--require-complete` (CI's mode) a missing design-system entry + * fails the run instead, naming the toolkit, before this function is ever + * called. See `DataMerger.assertNoMissingMetadata`. + */ const getDefaultMetadata = (toolkitId: string): MergedToolkitMetadata => applyToolkitTypeOverrides(toolkitId, { - category: "development", + category: DEFAULT_METADATA_CATEGORY, iconUrl: `https://design-system.arcade.dev/icons/${getDefaultIconId(toolkitId)}.svg`, isBYOC: false, isPro: false, type: "arcade", - docsLink: `https://docs.arcade.dev/en/mcp-servers/development/${getDefaultDocsSlug(toolkitId)}`, + docsLink: `https://docs.arcade.dev/en/resources/integrations/${DEFAULT_METADATA_CATEGORY}/${getDefaultDocsSlug(toolkitId)}`, isComingSoon: false, - isHidden: false, + isHidden: true, }); /** @@ -914,7 +980,12 @@ export const mergeToolkit = async ( warnings.push(...formatFreshnessWarnings(freshnessResult)); } - return { toolkit, warnings, failedTools }; + return { + toolkit, + warnings, + failedTools, + usedDefaultMetadata: metadata === null, + }; }; // ============================================================================ @@ -948,6 +1019,7 @@ export class DataMerger { | undefined; private readonly skipToolkitIds: ReadonlySet; private readonly requireCompleteData: boolean; + private readonly preserveLastKnownGood: boolean; private readonly resolveProviderId: | ((toolkitId: string) => string | null) | undefined; @@ -966,6 +1038,7 @@ export class DataMerger { this.onToolkitComplete = config.onToolkitComplete; this.skipToolkitIds = config.skipToolkitIds ?? new Set(); this.requireCompleteData = config.requireCompleteData ?? false; + this.preserveLastKnownGood = config.preserveLastKnownGood ?? false; this.resolveProviderId = config.resolveProviderId; } @@ -991,9 +1064,16 @@ export class DataMerger { warnings: [`Error processing toolkit: ${message}`], failedTools: [], error: message, + recovery: "preserved", + usedDefaultMetadata: false, }; } + // No previous toolkit to fall back on: this is a first-time toolkit + // whose merge threw before metadata even entered the picture. The + // placeholder below reuses the same "unhidden" category and forced + // `isHidden: true` as `getDefaultMetadata` and for the same reason — + // it's a guess, not a fact, so it must not be routable. return { toolkit: { id: toolkitId, @@ -1001,14 +1081,14 @@ export class DataMerger { version: "0.0.0", description: null, metadata: { - category: "development", + category: DEFAULT_METADATA_CATEGORY, iconUrl: "", isBYOC: false, isPro: false, type: isApiSuffixedToolkitId(toolkitId) ? "arcade_starter" : "arcade", docsLink: "", isComingSoon: false, - isHidden: false, + isHidden: true, }, auth: null, tools: [], @@ -1020,17 +1100,46 @@ export class DataMerger { warnings: [`Error processing toolkit: ${message}`], failedTools: [], error: message, + recovery: "omitted", + usedDefaultMetadata: true, }; } + private async recoverMissingMetadata( + toolkitId: string, + toolkitData: ToolkitData + ): Promise { + if (!this.preserveLastKnownGood || toolkitData.metadata !== null) { + return; + } + + const previousToolkit = this.getPreviousToolkit(toolkitId); + const result = this.buildMergeErrorResult( + toolkitId, + "missing design-system metadata", + previousToolkit + ); + if (this.onToolkitComplete && previousToolkit) { + await this.onToolkitComplete(result); + } + return result; + } + private async mergeToolkitEntry( toolkitId: string, toolkitData: ToolkitData ): Promise { try { + const recovered = await this.recoverMissingMetadata( + toolkitId, + toolkitData + ); + if (recovered) { + return recovered; + } + const customSections = await this.customSectionsSource.getCustomSections(toolkitId); - const previousToolkit = this.getPreviousToolkit(toolkitId); const result = await mergeToolkit( toolkitId, @@ -1298,6 +1407,11 @@ export class DataMerger { version ); + const recovered = await this.recoverMissingMetadata(toolkitId, toolkitData); + if (recovered) { + return recovered; + } + // Fetch custom sections const customSections = await this.customSectionsSource.getCustomSections(toolkitId); @@ -1323,20 +1437,43 @@ export class DataMerger { return result; } + /** + * Under `--require-complete`, a toolkit with no design-system metadata + * must fail the run instead of silently falling back to + * `getDefaultMetadata`'s guessed category/docsLink/icon. Silently + * dropping the toolkit (the old behavior) is just as bad as fabricating + * data for it — either way nobody finds out until a human notices a + * toolkit is missing or mis-filed. Naming every affected toolkit in one + * error, before any concurrent processing starts, keeps CI logs + * unambiguous about exactly what to fix upstream. + */ + private assertNoMissingMetadata( + toolkitEntries: ReadonlyArray + ): void { + if (!this.requireCompleteData) { + return; + } + + assertRequireCompleteMetadata(toolkitEntries); + } + /** * Merge data for all toolkits */ async mergeAllToolkits(): Promise { const allToolkitsData = await this.toolkitDataSource.fetchAllToolkitsData(); - const toolkitEntries = Array.from(allToolkitsData.entries()); - // Filter out toolkits that should be skipped (for resume support) + this.assertNoMissingMetadata(toolkitEntries); + + // Filter out toolkits that should be skipped (for resume support) and, + // under --require-complete, toolkits with no tools. Missing metadata is + // no longer filtered here — assertNoMissingMetadata above already threw + // if any slipped through. const filteredEntries = toolkitEntries.filter( ([toolkitId, toolkitData]) => !this.skipToolkitIds.has(toolkitId.toLowerCase()) && - (!this.requireCompleteData || - (toolkitData.metadata !== null && toolkitData.tools.length > 0)) + (!this.requireCompleteData || toolkitData.tools.length > 0) ); const results = await mapWithConcurrency( @@ -1369,12 +1506,15 @@ export class DataMerger { skipped: number; }> { const allToolkitsData = await this.toolkitDataSource.fetchAllToolkitsData(); + const toolkitEntries = Array.from(allToolkitsData.entries()); + + this.assertNoMissingMetadata(toolkitEntries); + const total = allToolkitsData.size; - const skipped = Array.from(allToolkitsData.entries()).filter( + const skipped = toolkitEntries.filter( ([id, toolkitData]) => this.skipToolkitIds.has(id.toLowerCase()) || - (this.requireCompleteData && - (toolkitData.metadata === null || toolkitData.tools.length === 0)) + (this.requireCompleteData && toolkitData.tools.length === 0) ).length; return { total, diff --git a/toolkit-docs-generator/src/merger/index.ts b/toolkit-docs-generator/src/merger/index.ts index 64bcd7804..4f497dd8d 100644 --- a/toolkit-docs-generator/src/merger/index.ts +++ b/toolkit-docs-generator/src/merger/index.ts @@ -1,5 +1,5 @@ /** * Merger module exports */ -export * from "./data-merger.js"; -export * from "./metadata-freshness.js"; +export * from "./data-merger"; +export * from "./metadata-freshness"; diff --git a/toolkit-docs-generator/src/merger/metadata-freshness.ts b/toolkit-docs-generator/src/merger/metadata-freshness.ts index 9528a243b..edfa19da2 100644 --- a/toolkit-docs-generator/src/merger/metadata-freshness.ts +++ b/toolkit-docs-generator/src/merger/metadata-freshness.ts @@ -7,7 +7,7 @@ * This is a modular, pure step that can run as part of the merge pipeline * or independently (e.g. in a CI check). */ -import type { MergedToolkit, MergedToolkitMetadata } from "../types/index.js"; +import type { MergedToolkit, MergedToolkitMetadata } from "../types/index"; // ============================================================================ // Types diff --git a/toolkit-docs-generator/src/merger/secret-coherence.ts b/toolkit-docs-generator/src/merger/secret-coherence.ts index 5ddeb83c0..a3d69c5de 100644 --- a/toolkit-docs-generator/src/merger/secret-coherence.ts +++ b/toolkit-docs-generator/src/merger/secret-coherence.ts @@ -13,7 +13,7 @@ * These scanners return structured issues. Remediation (LLM-driven edits or * warnings) is performed by callers in the merger pipeline. */ -import type { DocumentationChunk, MergedToolkit } from "../types/index.js"; +import type { DocumentationChunk, MergedToolkit } from "../types/index"; export const ARCADE_SECRETS_DOC_URL = "https://docs.arcade.dev/en/guides/create-tools/tool-basics/create-tool-secrets"; diff --git a/toolkit-docs-generator/src/shared/toolkit-schemas.ts b/toolkit-docs-generator/src/shared/toolkit-schemas.ts index 7531105b2..eb56aab02 100644 --- a/toolkit-docs-generator/src/shared/toolkit-schemas.ts +++ b/toolkit-docs-generator/src/shared/toolkit-schemas.ts @@ -19,7 +19,7 @@ * that shape, only the merged output defined here. */ import { z } from "zod"; -import { INTEGRATION_CATEGORIES } from "./toolkit-primitives.js"; +import { INTEGRATION_CATEGORIES } from "./toolkit-primitives"; // ============================================================================ // Tool Parameter Schema diff --git a/toolkit-docs-generator/src/sources/arcade-api.ts b/toolkit-docs-generator/src/sources/arcade-api.ts index 3d4d2c64f..f0c7dce9c 100644 --- a/toolkit-docs-generator/src/sources/arcade-api.ts +++ b/toolkit-docs-generator/src/sources/arcade-api.ts @@ -10,13 +10,13 @@ import type { ToolDefinition, ToolOutput, ToolParameter, -} from "../types/index.js"; +} from "../types/index"; import { type ArcadeTool, parseArcadeErrorResponse, parseArcadeToolsResponse, -} from "./arcade-api-types.js"; -import type { FetchOptions, IToolDataSource } from "./internal.js"; +} from "./arcade-api-types"; +import type { FetchOptions, IToolDataSource } from "./internal"; // ============================================================================ // Configuration diff --git a/toolkit-docs-generator/src/sources/custom-sections-file.ts b/toolkit-docs-generator/src/sources/custom-sections-file.ts index 2ed843062..624970f9f 100644 --- a/toolkit-docs-generator/src/sources/custom-sections-file.ts +++ b/toolkit-docs-generator/src/sources/custom-sections-file.ts @@ -6,13 +6,10 @@ */ import { access, readFile } from "fs/promises"; import { z } from "zod"; -import type { CustomSections } from "../types/index.js"; -import { - DocumentationChunkSchema, - ToolkitSubPageSchema, -} from "../types/index.js"; -import { normalizeId } from "../utils/fp.js"; -import type { ICustomSectionsSource } from "./interfaces.js"; +import type { CustomSections } from "../types/index"; +import { DocumentationChunkSchema, ToolkitSubPageSchema } from "../types/index"; +import { normalizeId } from "../utils/fp"; +import type { ICustomSectionsSource } from "./interfaces"; // ============================================================================ // File Schema diff --git a/toolkit-docs-generator/src/sources/design-system-metadata.ts b/toolkit-docs-generator/src/sources/design-system-metadata.ts index b25946660..e9f3e0864 100644 --- a/toolkit-docs-generator/src/sources/design-system-metadata.ts +++ b/toolkit-docs-generator/src/sources/design-system-metadata.ts @@ -8,10 +8,10 @@ */ import { TOOLKITS as DESIGN_SYSTEM_TOOLKITS } from "@arcadeai/design-system/metadata/toolkits"; import { z } from "zod"; -import { normalizeToolkitId } from "../shared/toolkit-primitives.js"; -import type { ToolkitMetadata } from "../types/index.js"; -import { ToolkitMetadataSchema } from "../types/index.js"; -import type { IMetadataSource } from "./internal.js"; +import { normalizeToolkitId } from "../shared/toolkit-primitives"; +import type { ToolkitMetadata } from "../types/index"; +import { ToolkitMetadataSchema } from "../types/index"; +import type { IMetadataSource } from "./internal"; // ============================================================================ // Types diff --git a/toolkit-docs-generator/src/sources/engine-api.ts b/toolkit-docs-generator/src/sources/engine-api.ts index eb5efee15..5b6862691 100644 --- a/toolkit-docs-generator/src/sources/engine-api.ts +++ b/toolkit-docs-generator/src/sources/engine-api.ts @@ -1,11 +1,11 @@ -import type { ToolDefinition } from "../types/index.js"; -import type { FetchOptions, IToolDataSource } from "./internal.js"; +import type { ToolDefinition } from "../types/index"; +import type { FetchOptions, IToolDataSource } from "./internal"; import { parseToolMetadataError, parseToolMetadataResponse, parseToolMetadataSummaryResponse, type ToolMetadataSummary, -} from "./tool-metadata-schema.js"; +} from "./tool-metadata-schema"; export interface EngineApiSourceConfig { /** Base URL for Engine (e.g., https://api.arcade.dev) */ diff --git a/toolkit-docs-generator/src/sources/in-memory.ts b/toolkit-docs-generator/src/sources/in-memory.ts index 8c7c47973..1ea321b65 100644 --- a/toolkit-docs-generator/src/sources/in-memory.ts +++ b/toolkit-docs-generator/src/sources/in-memory.ts @@ -9,14 +9,14 @@ import type { CustomSections, ToolDefinition, ToolkitMetadata, -} from "../types/index.js"; -import { normalizeId } from "../utils/fp.js"; -import type { ICustomSectionsSource } from "./interfaces.js"; +} from "../types/index"; +import { normalizeId } from "../utils/fp"; +import type { ICustomSectionsSource } from "./interfaces"; import type { FetchOptions, IMetadataSource, IToolDataSource, -} from "./internal.js"; +} from "./internal"; // ============================================================================ // In-Memory Tool Data Source diff --git a/toolkit-docs-generator/src/sources/index.ts b/toolkit-docs-generator/src/sources/index.ts index 5ce70d081..9354aa112 100644 --- a/toolkit-docs-generator/src/sources/index.ts +++ b/toolkit-docs-generator/src/sources/index.ts @@ -2,16 +2,16 @@ * Data sources exports */ -export * from "./arcade-api.js"; -export * from "./arcade-api-types.js"; -export * from "./custom-sections-file.js"; -export * from "./design-system-metadata.js"; -export * from "./engine-api.js"; -export * from "./in-memory.js"; -export * from "./interfaces.js"; -export * from "./mock-engine-api.js"; -export * from "./mock-metadata.js"; -export * from "./oauth-provider-resolver.js"; -export * from "./toolkit-data-source.js"; +export * from "./arcade-api"; +export * from "./arcade-api-types"; +export * from "./custom-sections-file"; +export * from "./design-system-metadata"; +export * from "./engine-api"; +export * from "./in-memory"; +export * from "./interfaces"; +export * from "./mock-engine-api"; +export * from "./mock-metadata"; +export * from "./oauth-provider-resolver"; +export * from "./toolkit-data-source"; // Note: Design System source requires @arcadeai/design-system to be installed. diff --git a/toolkit-docs-generator/src/sources/interfaces.ts b/toolkit-docs-generator/src/sources/interfaces.ts index 8cead5925..82662645d 100644 --- a/toolkit-docs-generator/src/sources/interfaces.ts +++ b/toolkit-docs-generator/src/sources/interfaces.ts @@ -3,7 +3,7 @@ * */ -import type { CustomSections } from "../types/index.js"; +import type { CustomSections } from "../types/index"; // ============================================================================ // Custom Sections Source Interface diff --git a/toolkit-docs-generator/src/sources/internal.ts b/toolkit-docs-generator/src/sources/internal.ts index dba59d7f9..f628d9249 100644 --- a/toolkit-docs-generator/src/sources/internal.ts +++ b/toolkit-docs-generator/src/sources/internal.ts @@ -4,7 +4,7 @@ * These interfaces are used only inside the toolkit data source implementations. * Do not export them from the public sources index. */ -import type { ToolDefinition, ToolkitMetadata } from "../types/index.js"; +import type { ToolDefinition, ToolkitMetadata } from "../types/index"; // ============================================================================ // Fetch Options diff --git a/toolkit-docs-generator/src/sources/mock-engine-api.ts b/toolkit-docs-generator/src/sources/mock-engine-api.ts index cb681eebb..00c958c59 100644 --- a/toolkit-docs-generator/src/sources/mock-engine-api.ts +++ b/toolkit-docs-generator/src/sources/mock-engine-api.ts @@ -6,10 +6,10 @@ * when the API endpoint is ready. */ import { readFile } from "fs/promises"; -import type { ToolDefinition } from "../types/index.js"; -import { normalizeId } from "../utils/fp.js"; -import type { FetchOptions, IToolDataSource } from "./internal.js"; -import { parseToolMetadataResponse } from "./tool-metadata-schema.js"; +import type { ToolDefinition } from "../types/index"; +import { normalizeId } from "../utils/fp"; +import type { FetchOptions, IToolDataSource } from "./internal"; +import { parseToolMetadataResponse } from "./tool-metadata-schema"; export interface MockEngineApiConfig { /** Path to the JSON fixture file */ diff --git a/toolkit-docs-generator/src/sources/mock-metadata.ts b/toolkit-docs-generator/src/sources/mock-metadata.ts index 68bc44a3f..cdb88ddd0 100644 --- a/toolkit-docs-generator/src/sources/mock-metadata.ts +++ b/toolkit-docs-generator/src/sources/mock-metadata.ts @@ -7,10 +7,10 @@ */ import { access, readFile } from "fs/promises"; import { z } from "zod"; -import type { ToolkitMetadata } from "../types/index.js"; -import { ToolkitMetadataSchema } from "../types/index.js"; -import { normalizeId } from "../utils/fp.js"; -import type { IMetadataSource } from "./internal.js"; +import type { ToolkitMetadata } from "../types/index"; +import { ToolkitMetadataSchema } from "../types/index"; +import { normalizeId } from "../utils/fp"; +import type { IMetadataSource } from "./internal"; // ============================================================================ // File Schema diff --git a/toolkit-docs-generator/src/sources/tool-metadata-schema.ts b/toolkit-docs-generator/src/sources/tool-metadata-schema.ts index 9b9727bc6..5ae406fe8 100644 --- a/toolkit-docs-generator/src/sources/tool-metadata-schema.ts +++ b/toolkit-docs-generator/src/sources/tool-metadata-schema.ts @@ -1,5 +1,5 @@ import { z } from "zod"; -import type { ToolDefinition, ToolParameter } from "../types/index.js"; +import type { ToolDefinition, ToolParameter } from "../types/index"; const ToolMetadataValueSchema = z.object({ val_type: z.string(), @@ -166,7 +166,7 @@ const normalizeSecrets = ( type RawBehavior = z.infer; -import type { ToolMetadataBehavior } from "../types/index.js"; +import type { ToolMetadataBehavior } from "../types/index"; const transformBehavior = ( raw: RawBehavior | null | undefined diff --git a/toolkit-docs-generator/src/sources/toolkit-data-source.ts b/toolkit-docs-generator/src/sources/toolkit-data-source.ts index de78a8951..7c7ac3a84 100644 --- a/toolkit-docs-generator/src/sources/toolkit-data-source.ts +++ b/toolkit-docs-generator/src/sources/toolkit-data-source.ts @@ -7,20 +7,20 @@ */ import { join } from "path"; -import { isApiSuffixedToolkitId } from "../shared/toolkit-primitives.js"; -import type { ToolDefinition, ToolkitMetadata } from "../types/index.js"; -import { filterToolsByHighestVersion } from "../utils/version-coherence.js"; +import { isApiSuffixedToolkitId } from "../shared/toolkit-primitives"; +import type { ToolDefinition, ToolkitMetadata } from "../types/index"; +import { filterToolsByHighestVersion } from "../utils/version-coherence"; import { type ArcadeApiSourceConfig, createArcadeApiSource, -} from "./arcade-api.js"; +} from "./arcade-api"; import { createEngineApiSource, type EngineApiSourceConfig, -} from "./engine-api.js"; -import type { IMetadataSource, IToolDataSource } from "./internal.js"; -import { createMockEngineApiSource } from "./mock-engine-api.js"; -import { createMockMetadataSource } from "./mock-metadata.js"; +} from "./engine-api"; +import type { IMetadataSource, IToolDataSource } from "./internal"; +import { createMockEngineApiSource } from "./mock-engine-api"; +import { createMockMetadataSource } from "./mock-metadata"; // ============================================================================ // Unified Toolkit Data Interface diff --git a/toolkit-docs-generator/src/types/index.ts b/toolkit-docs-generator/src/types/index.ts index 0e9e431a6..0990a2f28 100644 --- a/toolkit-docs-generator/src/types/index.ts +++ b/toolkit-docs-generator/src/types/index.ts @@ -20,9 +20,9 @@ import { ToolMetadataSchema, ToolOutputSchema, ToolParameterSchema, -} from "../shared/toolkit-schemas.js"; +} from "../shared/toolkit-schemas"; -export * from "../shared/toolkit-schemas.js"; +export * from "../shared/toolkit-schemas"; // ============================================================================ // CLI Input Types diff --git a/toolkit-docs-generator/src/utils/index.ts b/toolkit-docs-generator/src/utils/index.ts index 39d7aa24d..b1ac4504b 100644 --- a/toolkit-docs-generator/src/utils/index.ts +++ b/toolkit-docs-generator/src/utils/index.ts @@ -2,15 +2,15 @@ * Utility exports */ -export * from "./concurrency.js"; -export { removeExcludedToolkitFiles } from "./excluded-output-cleanup.js"; -export { readExclusionList } from "./exclusion-list.js"; -export * from "./fp.js"; -export { readIgnoreList } from "./ignore-list.js"; -export * from "./logger.js"; -export * from "./progress.js"; -export * from "./retry.js"; +export * from "./concurrency"; +export { removeExcludedToolkitFiles } from "./excluded-output-cleanup"; +export { readExclusionList } from "./exclusion-list"; +export * from "./fp"; +export { readIgnoreList } from "./ignore-list"; +export * from "./logger"; +export * from "./progress"; +export * from "./retry"; export { filterToolsByHighestVersion, getHighestVersion, -} from "./version-coherence.js"; +} from "./version-coherence"; diff --git a/toolkit-docs-generator/src/utils/provider-matching.ts b/toolkit-docs-generator/src/utils/provider-matching.ts index f03afe4a9..99b2912cc 100644 --- a/toolkit-docs-generator/src/utils/provider-matching.ts +++ b/toolkit-docs-generator/src/utils/provider-matching.ts @@ -10,7 +10,7 @@ * toolkit metadata IDs. */ -import type { ProviderVersion } from "../types/index.js"; +import type { ProviderVersion } from "../types/index"; const PROVIDER_LOOKUP_KEY_REGEX = /[^a-z0-9]/g; diff --git a/toolkit-docs-generator/src/utils/run-logs.ts b/toolkit-docs-generator/src/utils/run-logs.ts index 47a4d11a6..ecf4045d5 100644 --- a/toolkit-docs-generator/src/utils/run-logs.ts +++ b/toolkit-docs-generator/src/utils/run-logs.ts @@ -26,6 +26,8 @@ export interface FailedToolsReport { readonly generatedAt: string; readonly toolkits: readonly string[]; readonly failedToolkits: readonly string[]; + readonly preservedToolkits?: readonly string[]; + readonly omittedToolkits?: readonly string[]; readonly tools: readonly FailedToolEntry[]; } diff --git a/toolkit-docs-generator/src/utils/version-coherence.ts b/toolkit-docs-generator/src/utils/version-coherence.ts index 458ed0273..57d7ca627 100644 --- a/toolkit-docs-generator/src/utils/version-coherence.ts +++ b/toolkit-docs-generator/src/utils/version-coherence.ts @@ -1,5 +1,5 @@ -import type { ToolDefinition } from "../types/index.js"; -import { extractVersion } from "./fp.js"; +import type { ToolDefinition } from "../types/index"; +import { extractVersion } from "./fp"; interface ParsedSemver { readonly core: readonly number[]; diff --git a/toolkit-docs-generator/tests/cli/api-source.test.ts b/toolkit-docs-generator/tests/cli/api-source.test.ts index d1c137f01..18237cafb 100644 --- a/toolkit-docs-generator/tests/cli/api-source.test.ts +++ b/toolkit-docs-generator/tests/cli/api-source.test.ts @@ -1,5 +1,5 @@ import { afterEach, beforeEach, describe, expect, it } from "vitest"; -import { resolveApiSource } from "../../src/cli/api-source.js"; +import { resolveApiSource } from "../../src/cli/api-source"; const ORIGINAL_ENV = { ...process.env }; diff --git a/toolkit-docs-generator/tests/cli/exclusion-cleanup.test.ts b/toolkit-docs-generator/tests/cli/exclusion-cleanup.test.ts index 093f70b65..0982acbc5 100644 --- a/toolkit-docs-generator/tests/cli/exclusion-cleanup.test.ts +++ b/toolkit-docs-generator/tests/cli/exclusion-cleanup.test.ts @@ -2,7 +2,7 @@ import { mkdtemp, readFile, rm, writeFile } from "fs/promises"; import { tmpdir } from "os"; import { join } from "path"; import { afterEach, describe, expect, it, vi } from "vitest"; -import { cleanupExcludedToolkitOutput } from "../../src/cli/exclusion-cleanup.js"; +import { cleanupExcludedToolkitOutput } from "../../src/cli/exclusion-cleanup"; let tmpDir: string; diff --git a/toolkit-docs-generator/tests/cli/generate-flow.test.ts b/toolkit-docs-generator/tests/cli/generate-flow.test.ts index 12440b99a..75257a978 100644 --- a/toolkit-docs-generator/tests/cli/generate-flow.test.ts +++ b/toolkit-docs-generator/tests/cli/generate-flow.test.ts @@ -4,8 +4,10 @@ import { collectRemovedToolkitIds, computeProcessingStats, filterProvidersBySkipIds, -} from "../../src/cli/generate-flow.js"; -import type { ChangeDetectionResult } from "../../src/diff/index.js"; +} from "../../src/cli/generate-flow"; +import type { ChangeDetectionResult } from "../../src/diff/index"; +import { assertRequireCompleteMetadata } from "../../src/merger/data-merger"; +import type { ToolkitData } from "../../src/sources/toolkit-data-source"; // ── Minimal ChangeDetectionResult builder ───────────────────────────────────── @@ -162,7 +164,7 @@ describe("filterProvidersBySkipIds", () => { ); expect(providersToProcess).toHaveLength(1); - expect(providersToProcess[0].provider).toBe("Slack"); + expect(providersToProcess[0]?.provider).toBe("Slack"); expect(skippedProviders).toHaveLength(2); }); @@ -215,3 +217,53 @@ describe("filterProvidersBySkipIds", () => { expect(providerNames).toContain("Jira"); }); }); + +describe("assertRequireCompleteMetadata", () => { + it("throws when any toolkit is missing design-system metadata", () => { + const complete: ToolkitData = { + tools: [], + metadata: { + id: "Github", + label: "Github", + category: "development", + iconUrl: "https://example.com/icon.svg", + isBYOC: false, + isPro: false, + type: "arcade", + docsLink: "https://docs.example.com", + isComingSoon: false, + isHidden: false, + }, + }; + const missing: ToolkitData = { tools: [], metadata: null }; + + expect(() => + assertRequireCompleteMetadata([ + ["Github", complete], + ["Unknown", missing], + ]) + ).toThrow(/missing design-system metadata.*Unknown/); + }); + + it("passes when every toolkit has metadata", () => { + const complete: ToolkitData = { + tools: [], + metadata: { + id: "Github", + label: "Github", + category: "development", + iconUrl: "https://example.com/icon.svg", + isBYOC: false, + isPro: false, + type: "arcade", + docsLink: "https://docs.example.com", + isComingSoon: false, + isHidden: false, + }, + }; + + expect(() => + assertRequireCompleteMetadata([["Github", complete]]) + ).not.toThrow(); + }); +}); diff --git a/toolkit-docs-generator/tests/diff/previous-output.test.ts b/toolkit-docs-generator/tests/diff/previous-output.test.ts index b26899324..a59004108 100644 --- a/toolkit-docs-generator/tests/diff/previous-output.test.ts +++ b/toolkit-docs-generator/tests/diff/previous-output.test.ts @@ -1,7 +1,7 @@ import { describe, expect, it } from "vitest"; -import { parsePreviousToolkitForDiff } from "../../src/diff/previous-output.js"; -import type { MergedToolkit } from "../../src/types/index.js"; +import { parsePreviousToolkitForDiff } from "../../src/diff/previous-output"; +import type { MergedToolkit } from "../../src/types/index"; const createValidToolkit = (): MergedToolkit => ({ id: "Github", @@ -25,7 +25,6 @@ const createValidToolkit = (): MergedToolkit => ({ qualifiedName: "Github.CreateIssue", fullyQualifiedName: "Github.CreateIssue@1.0.0", description: null, - toolkitDescription: null, parameters: [ { name: "title", diff --git a/toolkit-docs-generator/tests/diff/summary-diff.test.ts b/toolkit-docs-generator/tests/diff/summary-diff.test.ts index 39e00ab99..36d434fa0 100644 --- a/toolkit-docs-generator/tests/diff/summary-diff.test.ts +++ b/toolkit-docs-generator/tests/diff/summary-diff.test.ts @@ -6,8 +6,8 @@ import { getChangedToolkitIdsFromSummary, hasSummaryChanges, type SummaryToolkit, -} from "../../src/diff/index.js"; -import type { MergedToolkit } from "../../src/types/index.js"; +} from "../../src/diff/index"; +import type { MergedToolkit } from "../../src/types/index"; const createMergedToolkit = (id: string, version: string): MergedToolkit => ({ id, diff --git a/toolkit-docs-generator/tests/diff/toolkit-diff.test.ts b/toolkit-docs-generator/tests/diff/toolkit-diff.test.ts index 521d2c46d..8f9dbf4ca 100644 --- a/toolkit-docs-generator/tests/diff/toolkit-diff.test.ts +++ b/toolkit-docs-generator/tests/diff/toolkit-diff.test.ts @@ -10,13 +10,13 @@ import { formatDetailedChanges, getChangedToolkitIds, hasChanges, -} from "../../src/diff/toolkit-diff.js"; +} from "../../src/diff/toolkit-diff"; import type { MergedTool, MergedToolkit, ToolDefinition, ToolkitMetadata, -} from "../../src/types/index.js"; +} from "../../src/types/index"; // ============================================================================ // Test Fixtures diff --git a/toolkit-docs-generator/tests/generator/output-verifier.test.ts b/toolkit-docs-generator/tests/generator/output-verifier.test.ts index daabcb57c..f2c616f0a 100644 --- a/toolkit-docs-generator/tests/generator/output-verifier.test.ts +++ b/toolkit-docs-generator/tests/generator/output-verifier.test.ts @@ -6,8 +6,8 @@ import { describe, expect, it } from "vitest"; import { createJsonGenerator, verifyOutputDir, -} from "../../src/generator/index.js"; -import type { MergedToolkit } from "../../src/types/index.js"; +} from "../../src/generator/index"; +import type { MergedToolkit } from "../../src/types/index"; const loadFixture = async (fileName: string): Promise => { const fixturesDir = new URL("../fixtures/", import.meta.url); diff --git a/toolkit-docs-generator/tests/llm/secret-edit-generator.test.ts b/toolkit-docs-generator/tests/llm/secret-edit-generator.test.ts index c8f341d3d..f70e67ff8 100644 --- a/toolkit-docs-generator/tests/llm/secret-edit-generator.test.ts +++ b/toolkit-docs-generator/tests/llm/secret-edit-generator.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it, vi } from "vitest"; -import type { LlmClient } from "../../src/llm/client.js"; -import { LlmSecretEditGenerator } from "../../src/llm/secret-edit-generator.js"; +import type { LlmClient } from "../../src/llm/client"; +import { LlmSecretEditGenerator } from "../../src/llm/secret-edit-generator"; const fakeClient = (response: string): LlmClient => ({ provider: "anthropic", diff --git a/toolkit-docs-generator/tests/llm/tool-example-generator.test.ts b/toolkit-docs-generator/tests/llm/tool-example-generator.test.ts index d898b4e3c..73635a64f 100644 --- a/toolkit-docs-generator/tests/llm/tool-example-generator.test.ts +++ b/toolkit-docs-generator/tests/llm/tool-example-generator.test.ts @@ -2,9 +2,9 @@ * Tests for the LLM tool example generator */ import { describe, expect, it } from "vitest"; -import type { LlmClient } from "../../src/llm/client.js"; -import { LlmToolExampleGenerator } from "../../src/llm/tool-example-generator.js"; -import type { ToolDefinition } from "../../src/types/index.js"; +import type { LlmClient } from "../../src/llm/client"; +import { LlmToolExampleGenerator } from "../../src/llm/tool-example-generator"; +import type { ToolDefinition } from "../../src/types/index"; const createTool = ( overrides: Partial = {} diff --git a/toolkit-docs-generator/tests/llm/toolkit-summary-generator.test.ts b/toolkit-docs-generator/tests/llm/toolkit-summary-generator.test.ts index 946159171..b313fd7e7 100644 --- a/toolkit-docs-generator/tests/llm/toolkit-summary-generator.test.ts +++ b/toolkit-docs-generator/tests/llm/toolkit-summary-generator.test.ts @@ -1,7 +1,7 @@ import { describe, expect, it } from "vitest"; -import type { LlmClient } from "../../src/llm/client.js"; -import { LlmToolkitSummaryGenerator } from "../../src/llm/toolkit-summary-generator.js"; -import type { MergedToolkit } from "../../src/types/index.js"; +import type { LlmClient } from "../../src/llm/client"; +import { LlmToolkitSummaryGenerator } from "../../src/llm/toolkit-summary-generator"; +import type { MergedToolkit } from "../../src/types/index"; const createToolkit = ( overrides: Partial = {} @@ -53,6 +53,7 @@ const createToolkit = ( describe("LlmToolkitSummaryGenerator", () => { it("parses summary from a JSON response", async () => { const client: LlmClient = { + provider: "openai", generateText: async () => '```json\n{"summary":"Concise summary."}\n```', }; const generator = new LlmToolkitSummaryGenerator({ @@ -68,6 +69,7 @@ describe("LlmToolkitSummaryGenerator", () => { it("includes tool descriptions and auth info in the prompt", async () => { let capturedPrompt = ""; const client: LlmClient = { + provider: "openai", generateText: async ({ prompt }) => { capturedPrompt = prompt; return '{"summary":"OK"}'; diff --git a/toolkit-docs-generator/tests/merger/data-merger.test.ts b/toolkit-docs-generator/tests/merger/data-merger.test.ts index e2277f399..1dc26bd2d 100644 --- a/toolkit-docs-generator/tests/merger/data-merger.test.ts +++ b/toolkit-docs-generator/tests/merger/data-merger.test.ts @@ -5,7 +5,7 @@ * the merge logic works correctly. */ import { describe, expect, it, vi } from "vitest"; -import type { ISecretEditGenerator } from "../../src/llm/secret-edit-generator.js"; +import type { ISecretEditGenerator } from "../../src/llm/secret-edit-generator"; import { computeAllScopes, DataMerger, @@ -15,24 +15,25 @@ import { mergeToolkit, type ToolExampleGenerator, type ToolkitSummaryGenerator, -} from "../../src/merger/data-merger.js"; +} from "../../src/merger/data-merger"; import { EmptyCustomSectionsSource, InMemoryCustomSectionsSource, InMemoryMetadataSource, InMemoryToolDataSource, -} from "../../src/sources/in-memory.js"; +} from "../../src/sources/in-memory"; +import type { ICustomSectionsSource } from "../../src/sources/interfaces"; import { createCombinedToolkitDataSource, type IToolkitDataSource, type ToolkitData, -} from "../../src/sources/toolkit-data-source.js"; +} from "../../src/sources/toolkit-data-source"; import type { CustomSections, ToolDefinition, ToolkitMetadata, -} from "../../src/types/index.js"; -import { extractVersion } from "../../src/utils/index.js"; +} from "../../src/types/index"; +import { extractVersion } from "../../src/utils/index"; // ============================================================================ // Test Fixtures - Realistic data matching production schema @@ -440,9 +441,33 @@ describe("mergeToolkit", () => { expect(result.toolkit.label).toBe("Unknown"); expect(result.toolkit.metadata.category).toBe("development"); + // The fabricated category can't be trusted enough to route or display — + // isHidden neutralizes it until real design-system metadata arrives. + expect(result.toolkit.metadata.isHidden).toBe(true); + // Must match the real integration route shape (see + // app/_lib/toolkit-static-params.ts getToolkitCanonicalPath), never the + // retired /en/mcp-servers/ prefix. + expect(result.toolkit.metadata.docsLink).toBe( + "https://docs.arcade.dev/en/resources/integrations/development/unknown" + ); expect(result.warnings).toContain( "No metadata found for toolkit: Unknown - using defaults" ); + expect(result.usedDefaultMetadata).toBe(true); + }); + + it("does not flag usedDefaultMetadata when design-system metadata is present", async () => { + const tools = [createTool({ qualifiedName: "TestKit.Tool1" })]; + + const result = await mergeToolkit( + "TestKit", + tools, + createMetadata(), + null, + createStubGenerator() + ); + + expect(result.usedDefaultMetadata).toBe(false); }); it("infers a readable label from toolkit description without metadata", async () => { @@ -1442,7 +1467,7 @@ describe("DataMerger", () => { }, ]; - const cleanupSpy = vi.fn( + const cleanupSpy = vi.fn( async () => "| Secret | Required For |\n| `GITHUB_SERVER_URL` | All tools |" ); @@ -1467,14 +1492,11 @@ describe("DataMerger", () => { const result = await merger.mergeToolkit("Github"); expect(cleanupSpy).toHaveBeenCalledTimes(1); - const cleanupCall = cleanupSpy.mock.calls[0]?.[0] as { - removedSecrets: string[]; - kind: string; - }; - expect(cleanupCall.removedSecrets).toEqual([ + const cleanupCall = cleanupSpy.mock.calls[0]?.[0]; + expect(cleanupCall?.removedSecrets).toEqual([ "GITHUB_CLASSIC_PERSONAL_ACCESS_TOKEN", ]); - expect(cleanupCall.kind).toBe("documentation_chunk"); + expect(cleanupCall?.kind).toBe("documentation_chunk"); // The chunk content in the result reflects the editor output. expect( result.toolkit.documentationChunks[0]?.content.includes( @@ -1887,10 +1909,13 @@ describe("DataMerger", () => { // buildMergeErrorResult is invoked by mergeToolkitEntry (called from // mergeAllToolkits). We trigger it by making the customSectionsSource throw, // which is caught by mergeToolkitEntry's try/catch. - const makeFailingCustomSectionsSource = () => ({ + const makeFailingCustomSectionsSource = (): ICustomSectionsSource => ({ getCustomSections: async () => { throw new Error("Custom sections source unavailable"); }, + getAllCustomSections: async () => { + throw new Error("Custom sections source unavailable"); + }, }); it("preserves documentationChunks and customImports from previous toolkit when merge throws", async () => { @@ -2006,21 +2031,11 @@ describe("DataMerger", () => { expect(slackResult?.toolkit.tools).toHaveLength(1); }); - it("skips toolkits missing metadata or tools when requireCompleteData is true", async () => { + it("skips toolkits with no tools (but present metadata) when requireCompleteData is true", async () => { const completeToolkitData: ToolkitData = { tools: [githubTool1], metadata: githubMetadata, }; - const missingMetadataToolkitData: ToolkitData = { - tools: [ - createTool({ - name: "Lookup", - qualifiedName: "Unknown.Lookup", - fullyQualifiedName: "Unknown.Lookup@1.0.0", - }), - ], - metadata: null, - }; const missingToolsToolkitData: ToolkitData = { tools: [], metadata: slackMetadata, @@ -2031,9 +2046,6 @@ describe("DataMerger", () => { if (toolkitId === "Github") { return completeToolkitData; } - if (toolkitId === "Unknown") { - return missingMetadataToolkitData; - } if (toolkitId === "Slack") { return missingToolsToolkitData; } @@ -2042,7 +2054,6 @@ describe("DataMerger", () => { fetchAllToolkitsData: async () => new Map([ ["Github", completeToolkitData], - ["Unknown", missingMetadataToolkitData], ["Slack", missingToolsToolkitData], ]), isAvailable: async () => true, @@ -2058,13 +2069,217 @@ describe("DataMerger", () => { const count = await merger.getToolkitCount(); const results = await merger.mergeAllToolkits(); - expect(count.total).toBe(3); + expect(count.total).toBe(2); expect(count.toProcess).toBe(1); - expect(count.skipped).toBe(2); + expect(count.skipped).toBe(1); expect(results).toHaveLength(1); expect(results[0]?.toolkit.id).toBe("Github"); }); + it("preserves prior output when metadata is missing in resilient publishing mode", async () => { + const previous = await mergeToolkit( + "Github", + [githubTool1], + githubMetadata, + createCustomSections(), + createStubGenerator() + ); + const toolkitDataSource = createCombinedToolkitDataSource({ + toolSource: new InMemoryToolDataSource([githubTool1]), + metadataSource: new InMemoryMetadataSource([]), + }); + const merger = new DataMerger({ + toolkitDataSource, + customSectionsSource: new EmptyCustomSectionsSource(), + toolExampleGenerator: createStubGenerator(), + previousToolkits: new Map([["github", previous.toolkit]]), + preserveLastKnownGood: true, + }); + + const [result] = await merger.mergeAllToolkits(); + + expect(result?.recovery).toBe("preserved"); + expect(result?.toolkit).toEqual(previous.toolkit); + expect(result?.error).toContain("missing design-system metadata"); + }); + + it("preserves prior output for provider-mode generation when metadata is missing", async () => { + const previous = await mergeToolkit( + "Github", + [githubTool1], + githubMetadata, + createCustomSections(), + createStubGenerator() + ); + const toolkitDataSource = createCombinedToolkitDataSource({ + toolSource: new InMemoryToolDataSource([githubTool1]), + metadataSource: new InMemoryMetadataSource([]), + }); + const merger = new DataMerger({ + toolkitDataSource, + customSectionsSource: new EmptyCustomSectionsSource(), + toolExampleGenerator: createStubGenerator(), + previousToolkits: new Map([["github", previous.toolkit]]), + preserveLastKnownGood: true, + }); + + const result = await merger.mergeToolkit("Github"); + + expect(result.recovery).toBe("preserved"); + expect(result.toolkit).toEqual(previous.toolkit); + expect(result.error).toContain("missing design-system metadata"); + }); + + it("omits a new toolkit when metadata is missing in resilient publishing mode", async () => { + const toolkitDataSource = createCombinedToolkitDataSource({ + toolSource: new InMemoryToolDataSource([githubTool1]), + metadataSource: new InMemoryMetadataSource([]), + }); + const merger = new DataMerger({ + toolkitDataSource, + customSectionsSource: new EmptyCustomSectionsSource(), + toolExampleGenerator: createStubGenerator(), + preserveLastKnownGood: true, + }); + + const [result] = await merger.mergeAllToolkits(); + + expect(result?.recovery).toBe("omitted"); + expect(result?.error).toContain("missing design-system metadata"); + }); + + it("fails the run and names every toolkit missing design-system metadata when requireCompleteData is true", async () => { + // Silently dropping (the old behavior) or silently fabricating + // metadata for these toolkits are both worse than failing loudly: + // --require-complete exists so CI can't ship a wrong-but-valid + // category or a docsLink nobody chose. The error must name every + // affected toolkit, not just the first one found, so a single CI + // failure is enough to fix the whole batch. + const completeToolkitData: ToolkitData = { + tools: [githubTool1], + metadata: githubMetadata, + }; + const missingMetadataToolkitData: ToolkitData = { + tools: [ + createTool({ + name: "Lookup", + qualifiedName: "Unknown.Lookup", + fullyQualifiedName: "Unknown.Lookup@1.0.0", + }), + ], + metadata: null, + }; + const anotherMissingMetadataToolkitData: ToolkitData = { + tools: [ + createTool({ + name: "Ping", + qualifiedName: "AlsoUnknown.Ping", + fullyQualifiedName: "AlsoUnknown.Ping@1.0.0", + }), + ], + metadata: null, + }; + + const toolkitDataSource: IToolkitDataSource = { + fetchToolkitData: async () => { + throw new Error("not used by mergeAllToolkits"); + }, + fetchAllToolkitsData: async () => + new Map([ + ["Github", completeToolkitData], + ["Unknown", missingMetadataToolkitData], + ["AlsoUnknown", anotherMissingMetadataToolkitData], + ]), + isAvailable: async () => true, + }; + + const merger = new DataMerger({ + toolkitDataSource, + customSectionsSource: new EmptyCustomSectionsSource(), + toolExampleGenerator: createStubGenerator(), + requireCompleteData: true, + }); + + await expect(merger.mergeAllToolkits()).rejects.toThrow( + /missing design-system metadata.*Unknown.*AlsoUnknown/s + ); + await expect(merger.getToolkitCount()).rejects.toThrow( + /missing design-system metadata.*Unknown.*AlsoUnknown/s + ); + }); + + it("fails strict runs for toolkits in skipToolkitIds when metadata is missing", async () => { + const missingMetadataToolkitData: ToolkitData = { + tools: [ + createTool({ + name: "Lookup", + qualifiedName: "Unknown.Lookup", + fullyQualifiedName: "Unknown.Lookup@1.0.0", + }), + ], + metadata: null, + }; + + const toolkitDataSource: IToolkitDataSource = { + fetchToolkitData: async () => missingMetadataToolkitData, + fetchAllToolkitsData: async () => + new Map([["Unknown", missingMetadataToolkitData]]), + isAvailable: async () => true, + }; + + const merger = new DataMerger({ + toolkitDataSource, + customSectionsSource: new EmptyCustomSectionsSource(), + toolExampleGenerator: createStubGenerator(), + requireCompleteData: true, + skipToolkitIds: new Set(["unknown"]), + }); + + await expect(merger.mergeAllToolkits()).rejects.toThrow( + /missing design-system metadata.*Unknown/s + ); + await expect(merger.getToolkitCount()).rejects.toThrow( + /missing design-system metadata.*Unknown/s + ); + }); + + it("does not skip or fail toolkits missing metadata when requireCompleteData is false", async () => { + // Without --require-complete, generation must still complete — the + // toolkit falls back to getDefaultMetadata (hidden placeholder + // metadata) and usedDefaultMetadata reports the fact rather than the + // omission disappearing entirely. + const missingMetadataToolkitData: ToolkitData = { + tools: [ + createTool({ + name: "Lookup", + qualifiedName: "Unknown.Lookup", + fullyQualifiedName: "Unknown.Lookup@1.0.0", + }), + ], + metadata: null, + }; + + const toolkitDataSource: IToolkitDataSource = { + fetchToolkitData: async () => missingMetadataToolkitData, + fetchAllToolkitsData: async () => + new Map([["Unknown", missingMetadataToolkitData]]), + isAvailable: async () => true, + }; + + const merger = new DataMerger({ + toolkitDataSource, + customSectionsSource: new EmptyCustomSectionsSource(), + toolExampleGenerator: createStubGenerator(), + }); + + const results = await merger.mergeAllToolkits(); + + expect(results).toHaveLength(1); + expect(results[0]?.toolkit.id).toBe("Unknown"); + expect(results[0]?.usedDefaultMetadata).toBe(true); + expect(results[0]?.toolkit.metadata.isHidden).toBe(true); + }); + it("fails strict runs when a complete toolkit cannot be merged", async () => { const toolkitDataSource = createCombinedToolkitDataSource({ toolSource: new InMemoryToolDataSource([githubTool1]), @@ -2076,6 +2291,9 @@ describe("DataMerger", () => { getCustomSections: async () => { throw new Error("Custom sections source unavailable"); }, + getAllCustomSections: async () => { + throw new Error("Custom sections source unavailable"); + }, }, toolExampleGenerator: createStubGenerator(), requireCompleteData: true, diff --git a/toolkit-docs-generator/tests/merger/metadata-freshness.test.ts b/toolkit-docs-generator/tests/merger/metadata-freshness.test.ts index 4973bf9e5..498bb0512 100644 --- a/toolkit-docs-generator/tests/merger/metadata-freshness.test.ts +++ b/toolkit-docs-generator/tests/merger/metadata-freshness.test.ts @@ -9,11 +9,11 @@ import { detectMetadataChanges, formatFreshnessWarnings, type MetadataFreshnessResult, -} from "../../src/merger/metadata-freshness.js"; +} from "../../src/merger/metadata-freshness"; import type { MergedToolkit, MergedToolkitMetadata, -} from "../../src/types/index.js"; +} from "../../src/types/index"; // ============================================================================ // Fixtures @@ -34,7 +34,7 @@ const createMetadata = ( }); const createPreviousToolkit = ( - overrides: Partial & { + overrides: Omit, "metadata"> & { metadata?: Partial; label?: string; } = {} @@ -349,7 +349,7 @@ describe("mergeToolkit metadata freshness integration", () => { // covers the happy path extensively. it("emits warnings when previous toolkit metadata differs", async () => { - const { mergeToolkit } = await import("../../src/merger/data-merger.js"); + const { mergeToolkit } = await import("../../src/merger/data-merger"); const tool = { name: "TestTool", @@ -424,7 +424,7 @@ describe("mergeToolkit metadata freshness integration", () => { }); it("emits no freshness warnings when metadata is unchanged", async () => { - const { mergeToolkit } = await import("../../src/merger/data-merger.js"); + const { mergeToolkit } = await import("../../src/merger/data-merger"); const tool = { name: "TestTool", diff --git a/toolkit-docs-generator/tests/merger/secret-coherence.test.ts b/toolkit-docs-generator/tests/merger/secret-coherence.test.ts index efe5a686f..43e6f768e 100644 --- a/toolkit-docs-generator/tests/merger/secret-coherence.test.ts +++ b/toolkit-docs-generator/tests/merger/secret-coherence.test.ts @@ -6,12 +6,12 @@ import { detectStaleSecretReferences, groupStaleRefsByTarget, hasCoherenceIssues, -} from "../../src/merger/secret-coherence.js"; +} from "../../src/merger/secret-coherence"; import type { DocumentationChunk, MergedTool, MergedToolkit, -} from "../../src/types/index.js"; +} from "../../src/types/index"; const chunk = ( overrides: Partial = {} diff --git a/toolkit-docs-generator/tests/scenarios/failed-tools.test.ts b/toolkit-docs-generator/tests/scenarios/failed-tools.test.ts index 04441dbc9..7187bcee7 100644 --- a/toolkit-docs-generator/tests/scenarios/failed-tools.test.ts +++ b/toolkit-docs-generator/tests/scenarios/failed-tools.test.ts @@ -12,14 +12,14 @@ import { describe, expect, it } from "vitest"; import { createDataMerger, type ToolExampleGenerator, -} from "../../src/merger/data-merger.js"; +} from "../../src/merger/data-merger"; import { EmptyCustomSectionsSource, InMemoryMetadataSource, InMemoryToolDataSource, -} from "../../src/sources/in-memory.js"; -import { createCombinedToolkitDataSource } from "../../src/sources/toolkit-data-source.js"; -import type { ToolDefinition } from "../../src/types/index.js"; +} from "../../src/sources/in-memory"; +import { createCombinedToolkitDataSource } from "../../src/sources/toolkit-data-source"; +import type { ToolDefinition } from "../../src/types/index"; const createTool = ( overrides: Partial = {} diff --git a/toolkit-docs-generator/tests/scenarios/new-toolkit.test.ts b/toolkit-docs-generator/tests/scenarios/new-toolkit.test.ts index 0d15b949b..16658a225 100644 --- a/toolkit-docs-generator/tests/scenarios/new-toolkit.test.ts +++ b/toolkit-docs-generator/tests/scenarios/new-toolkit.test.ts @@ -11,15 +11,15 @@ import { tmpdir } from "os"; import { join } from "path"; import { describe, expect, it } from "vitest"; -import { createJsonGenerator } from "../../src/generator/index.js"; -import { createDataMerger } from "../../src/merger/data-merger.js"; +import { createJsonGenerator } from "../../src/generator/index"; +import { createDataMerger } from "../../src/merger/data-merger"; import { EmptyCustomSectionsSource, InMemoryMetadataSource, InMemoryToolDataSource, -} from "../../src/sources/in-memory.js"; -import { createCombinedToolkitDataSource } from "../../src/sources/toolkit-data-source.js"; -import type { ToolDefinition, ToolkitMetadata } from "../../src/types/index.js"; +} from "../../src/sources/in-memory"; +import { createCombinedToolkitDataSource } from "../../src/sources/toolkit-data-source"; +import type { ToolDefinition, ToolkitMetadata } from "../../src/types/index"; const createTool = ( overrides: Partial = {} diff --git a/toolkit-docs-generator/tests/scenarios/removed-toolkit-cleanup.test.ts b/toolkit-docs-generator/tests/scenarios/removed-toolkit-cleanup.test.ts index 8184ed93a..68c9811a3 100644 --- a/toolkit-docs-generator/tests/scenarios/removed-toolkit-cleanup.test.ts +++ b/toolkit-docs-generator/tests/scenarios/removed-toolkit-cleanup.test.ts @@ -14,11 +14,11 @@ import { mkdtemp, readdir, readFile, rm } from "node:fs/promises"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { afterEach, describe, expect, it } from "vitest"; -import { cleanupExcludedToolkitOutput } from "../../src/cli/exclusion-cleanup.js"; -import { collectRemovedToolkitIds } from "../../src/cli/generate-flow.js"; -import { detectChanges } from "../../src/diff/index.js"; -import { createJsonGenerator } from "../../src/generator/index.js"; -import type { MergedToolkit, ToolDefinition } from "../../src/types/index.js"; +import { cleanupExcludedToolkitOutput } from "../../src/cli/exclusion-cleanup"; +import { collectRemovedToolkitIds } from "../../src/cli/generate-flow"; +import { detectChanges } from "../../src/diff/index"; +import { createJsonGenerator } from "../../src/generator/index"; +import type { MergedToolkit, ToolDefinition } from "../../src/types/index"; // ── Fixtures ────────────────────────────────────────────────────────────────── diff --git a/toolkit-docs-generator/tests/scenarios/removed-toolkit.test.ts b/toolkit-docs-generator/tests/scenarios/removed-toolkit.test.ts index e0c8daaf9..1cce596f7 100644 --- a/toolkit-docs-generator/tests/scenarios/removed-toolkit.test.ts +++ b/toolkit-docs-generator/tests/scenarios/removed-toolkit.test.ts @@ -10,8 +10,8 @@ */ import { describe, expect, it } from "vitest"; -import { detectChanges } from "../../src/diff/index.js"; -import type { MergedToolkit, ToolDefinition } from "../../src/types/index.js"; +import { detectChanges } from "../../src/diff/index"; +import type { MergedToolkit, ToolDefinition } from "../../src/types/index"; const createMergedToolkit = (id: string): MergedToolkit => ({ id, diff --git a/toolkit-docs-generator/tests/scenarios/skip-unchanged.test.ts b/toolkit-docs-generator/tests/scenarios/skip-unchanged.test.ts index c09caf0c8..5214500de 100644 --- a/toolkit-docs-generator/tests/scenarios/skip-unchanged.test.ts +++ b/toolkit-docs-generator/tests/scenarios/skip-unchanged.test.ts @@ -9,8 +9,9 @@ import { detectChanges, getChangedToolkitIds, hasChanges, -} from "../../src/diff/index.js"; -import type { MergedToolkit, ToolDefinition } from "../../src/types/index.js"; +} from "../../src/diff/index"; +import type { CurrentToolkitDiffInput } from "../../src/diff/toolkit-diff"; +import type { MergedToolkit, ToolDefinition } from "../../src/types/index"; const createTool = ( overrides: Partial = {} @@ -197,7 +198,7 @@ describe("Scenario: Skip unchanged toolkits", () => { }); it("includes metadata-only changes in changed IDs", () => { - const currentToolkitData = new Map([ + const currentToolkitData = new Map([ [ "Github", { diff --git a/toolkit-docs-generator/tests/scenarios/stale-version-tools.test.ts b/toolkit-docs-generator/tests/scenarios/stale-version-tools.test.ts index 49d2dcc9f..72bb2e28f 100644 --- a/toolkit-docs-generator/tests/scenarios/stale-version-tools.test.ts +++ b/toolkit-docs-generator/tests/scenarios/stale-version-tools.test.ts @@ -11,13 +11,13 @@ import { detectChanges, getChangedToolkitIds, hasChanges, -} from "../../src/diff/index.js"; +} from "../../src/diff/index"; import { InMemoryMetadataSource, InMemoryToolDataSource, -} from "../../src/sources/in-memory.js"; -import { createCombinedToolkitDataSource } from "../../src/sources/toolkit-data-source.js"; -import type { MergedToolkit, ToolDefinition } from "../../src/types/index.js"; +} from "../../src/sources/in-memory"; +import { createCombinedToolkitDataSource } from "../../src/sources/toolkit-data-source"; +import type { MergedToolkit, ToolDefinition } from "../../src/types/index"; const createTool = ( overrides: Partial = {} diff --git a/toolkit-docs-generator/tests/scripts/sync-toolkit-sidebar.test.ts b/toolkit-docs-generator/tests/scripts/sync-toolkit-sidebar.test.ts index d147da8f2..c8b055ce9 100644 --- a/toolkit-docs-generator/tests/scripts/sync-toolkit-sidebar.test.ts +++ b/toolkit-docs-generator/tests/scripts/sync-toolkit-sidebar.test.ts @@ -381,12 +381,25 @@ describe("buildToolkitInfoList", () => { describe("groupByCategory", () => { it("should group toolkits by category", () => { const toolkits: ToolkitInfo[] = [ - { id: "gmail", slug: "gmail", label: "Gmail", category: "productivity" }, - { id: "slack", slug: "slack", label: "Slack", category: "social" }, + { + id: "gmail", + slug: "gmail", + label: "Gmail", + navGroup: "optimized", + category: "productivity", + }, + { + id: "slack", + slug: "slack", + label: "Slack", + navGroup: "optimized", + category: "social", + }, { id: "dropbox", slug: "dropbox", label: "Dropbox", + navGroup: "optimized", category: "productivity", }, ]; @@ -400,9 +413,27 @@ describe("groupByCategory", () => { it("should sort toolkits alphabetically by label", () => { const toolkits: ToolkitInfo[] = [ - { id: "zoom", slug: "zoom", label: "Zoom", category: "social" }, - { id: "slack", slug: "slack", label: "Slack", category: "social" }, - { id: "discord", slug: "discord", label: "Discord", category: "social" }, + { + id: "zoom", + slug: "zoom", + label: "Zoom", + navGroup: "optimized", + category: "social", + }, + { + id: "slack", + slug: "slack", + label: "Slack", + navGroup: "optimized", + category: "social", + }, + { + id: "discord", + slug: "discord", + label: "Discord", + navGroup: "optimized", + category: "social", + }, ]; const result = groupByCategory(toolkits); @@ -420,7 +451,13 @@ describe("groupByCategory", () => { it("should group toolkits by category", () => { const toolkits: ToolkitInfo[] = [ - { id: "custom", slug: "custom", label: "Custom", category: "payments" }, + { + id: "custom", + slug: "custom", + label: "Custom", + navGroup: "optimized", + category: "payments", + }, ]; const result = groupByCategory(toolkits); @@ -490,11 +527,18 @@ describe("remove empty section flags", () => { describe("generateCategoryMeta", () => { it("should generate valid _meta.tsx content", () => { const toolkits: ToolkitInfo[] = [ - { id: "gmail", slug: "gmail", label: "Gmail", category: "productivity" }, + { + id: "gmail", + slug: "gmail", + label: "Gmail", + navGroup: "optimized", + category: "productivity", + }, { id: "dropbox", slug: "dropbox", label: "Dropbox", + navGroup: "optimized", category: "productivity", }, ]; @@ -521,6 +565,7 @@ describe("generateCategoryMeta", () => { id: "test", slug: "test", label: 'Test "Quoted" Label', + navGroup: "optimized", category: "productivity", }, ]; @@ -540,7 +585,13 @@ describe("generateCategoryMeta", () => { it("should handle single toolkit", () => { const toolkits: ToolkitInfo[] = [ - { id: "gmail", slug: "gmail", label: "Gmail", category: "productivity" }, + { + id: "gmail", + slug: "gmail", + label: "Gmail", + navGroup: "optimized", + category: "productivity", + }, ]; const result = generateCategoryMeta(toolkits, "productivity", "/preview"); diff --git a/toolkit-docs-generator/tests/sources/arcade-api.test.ts b/toolkit-docs-generator/tests/sources/arcade-api.test.ts index d09122010..b09abb17e 100644 --- a/toolkit-docs-generator/tests/sources/arcade-api.test.ts +++ b/toolkit-docs-generator/tests/sources/arcade-api.test.ts @@ -1,10 +1,11 @@ +import type { Mock } from "vitest"; import { beforeEach, describe, expect, it, vi } from "vitest"; import { ArcadeApiSource, createArcadeApiSource, createProductionArcadeApiSource, -} from "../../src/sources/arcade-api.js"; -import type { ArcadeToolsResponse } from "../../src/sources/arcade-api-types.js"; +} from "../../src/sources/arcade-api"; +import type { ArcadeToolsResponse } from "../../src/sources/arcade-api-types"; // ============================================================================ // Test Fixtures @@ -214,11 +215,27 @@ const mockToolWithSecrets: ArcadeToolsResponse["items"][0] = { // Tests // ============================================================================ +/** + * ArcadeApiSource only reads `.ok`, `.status`, `.statusText`, `.headers.get(...)`, + * and `.json()` off the fetch response, so tests mock that subset and assert + * it as a `Response` rather than constructing a real one. + */ +type FetchResponseLike = { + ok: boolean; + status?: number; + statusText?: string; + headers?: { get(name: string): string | null | undefined }; + json?: () => Promise; +}; + +const asResponse = (value: FetchResponseLike): Response => + value as unknown as Response; + describe("ArcadeApiSource", () => { - let mockFetch: ReturnType; + let mockFetch: Mock; beforeEach(() => { - mockFetch = vi.fn(); + mockFetch = vi.fn(); }); describe("constructor and configuration", () => { @@ -232,10 +249,12 @@ describe("ArcadeApiSource", () => { }); it("should normalize base URL with trailing slash", () => { - mockFetch.mockResolvedValueOnce({ - ok: true, - json: () => Promise.resolve(createMockArcadeResponse([])), - }); + mockFetch.mockResolvedValueOnce( + asResponse({ + ok: true, + json: () => Promise.resolve(createMockArcadeResponse([])), + }) + ); const source = new ArcadeApiSource({ baseUrl: "https://api.arcade.dev/", @@ -252,10 +271,12 @@ describe("ArcadeApiSource", () => { }); it("should handle base URL with /v1 suffix", () => { - mockFetch.mockResolvedValueOnce({ - ok: true, - json: () => Promise.resolve(createMockArcadeResponse([])), - }); + mockFetch.mockResolvedValueOnce( + asResponse({ + ok: true, + json: () => Promise.resolve(createMockArcadeResponse([])), + }) + ); const source = new ArcadeApiSource({ baseUrl: "https://api.arcade.dev/v1", @@ -272,10 +293,12 @@ describe("ArcadeApiSource", () => { }); it("should cap page size at maximum", () => { - mockFetch.mockResolvedValueOnce({ - ok: true, - json: () => Promise.resolve(createMockArcadeResponse([])), - }); + mockFetch.mockResolvedValueOnce( + asResponse({ + ok: true, + json: () => Promise.resolve(createMockArcadeResponse([])), + }) + ); const source = new ArcadeApiSource({ baseUrl: "https://api.arcade.dev", @@ -293,13 +316,18 @@ describe("ArcadeApiSource", () => { describe("fetchAllTools", () => { it("should fetch and transform tools correctly", async () => { - mockFetch.mockResolvedValueOnce({ - ok: true, - json: () => - Promise.resolve( - createMockArcadeResponse([mockAirtableTool, mockGoogleCalendarTool]) - ), - }); + mockFetch.mockResolvedValueOnce( + asResponse({ + ok: true, + json: () => + Promise.resolve( + createMockArcadeResponse([ + mockAirtableTool, + mockGoogleCalendarTool, + ]) + ), + }) + ); const source = new ArcadeApiSource({ baseUrl: "https://api.arcade.dev", @@ -362,11 +390,13 @@ describe("ArcadeApiSource", () => { }); it("should handle tools with array parameters", async () => { - mockFetch.mockResolvedValueOnce({ - ok: true, - json: () => - Promise.resolve(createMockArcadeResponse([mockGoogleCalendarTool])), - }); + mockFetch.mockResolvedValueOnce( + asResponse({ + ok: true, + json: () => + Promise.resolve(createMockArcadeResponse([mockGoogleCalendarTool])), + }) + ); const source = new ArcadeApiSource({ baseUrl: "https://api.arcade.dev", @@ -391,11 +421,13 @@ describe("ArcadeApiSource", () => { }); it("should extract secrets from requirements", async () => { - mockFetch.mockResolvedValueOnce({ - ok: true, - json: () => - Promise.resolve(createMockArcadeResponse([mockToolWithSecrets])), - }); + mockFetch.mockResolvedValueOnce( + asResponse({ + ok: true, + json: () => + Promise.resolve(createMockArcadeResponse([mockToolWithSecrets])), + }) + ); const source = new ArcadeApiSource({ baseUrl: "https://api.arcade.dev", @@ -412,17 +444,19 @@ describe("ArcadeApiSource", () => { }); it("should filter by toolkit ID client-side", async () => { - mockFetch.mockResolvedValueOnce({ - ok: true, - json: () => - Promise.resolve( - createMockArcadeResponse([ - mockAirtableTool, - mockGoogleCalendarTool, - mockToolWithSecrets, - ]) - ), - }); + mockFetch.mockResolvedValueOnce( + asResponse({ + ok: true, + json: () => + Promise.resolve( + createMockArcadeResponse([ + mockAirtableTool, + mockGoogleCalendarTool, + mockToolWithSecrets, + ]) + ), + }) + ); const source = new ArcadeApiSource({ baseUrl: "https://api.arcade.dev", @@ -437,11 +471,13 @@ describe("ArcadeApiSource", () => { }); it("should filter by toolkit ID case-insensitively", async () => { - mockFetch.mockResolvedValueOnce({ - ok: true, - json: () => - Promise.resolve(createMockArcadeResponse([mockGoogleCalendarTool])), - }); + mockFetch.mockResolvedValueOnce( + asResponse({ + ok: true, + json: () => + Promise.resolve(createMockArcadeResponse([mockGoogleCalendarTool])), + }) + ); const source = new ArcadeApiSource({ baseUrl: "https://api.arcade.dev", @@ -455,17 +491,19 @@ describe("ArcadeApiSource", () => { }); it("should filter by version", async () => { - mockFetch.mockResolvedValueOnce({ - ok: true, - json: () => - Promise.resolve( - createMockArcadeResponse([ - mockAirtableTool, // @4.0.0 - mockGoogleCalendarTool, // @1.0.0 - mockToolWithSecrets, // @2.0.0 - ]) - ), - }); + mockFetch.mockResolvedValueOnce( + asResponse({ + ok: true, + json: () => + Promise.resolve( + createMockArcadeResponse([ + mockAirtableTool, // @4.0.0 + mockGoogleCalendarTool, // @1.0.0 + mockToolWithSecrets, // @2.0.0 + ]) + ), + }) + ); const source = new ArcadeApiSource({ baseUrl: "https://api.arcade.dev", @@ -485,28 +523,32 @@ describe("ArcadeApiSource", () => { describe("pagination", () => { it("should handle pagination correctly", async () => { // First page - mockFetch.mockResolvedValueOnce({ - ok: true, - json: () => - Promise.resolve({ - items: [mockAirtableTool], - limit: 1, - offset: 0, - total_count: 2, - }), - }); + mockFetch.mockResolvedValueOnce( + asResponse({ + ok: true, + json: () => + Promise.resolve({ + items: [mockAirtableTool], + limit: 1, + offset: 0, + total_count: 2, + }), + }) + ); // Second page - mockFetch.mockResolvedValueOnce({ - ok: true, - json: () => - Promise.resolve({ - items: [mockGoogleCalendarTool], - limit: 1, - offset: 1, - total_count: 2, - }), - }); + mockFetch.mockResolvedValueOnce( + asResponse({ + ok: true, + json: () => + Promise.resolve({ + items: [mockGoogleCalendarTool], + limit: 1, + offset: 1, + total_count: 2, + }), + }) + ); const source = new ArcadeApiSource({ baseUrl: "https://api.arcade.dev", @@ -522,27 +564,31 @@ describe("ArcadeApiSource", () => { }); it("should stop pagination when no more items", async () => { - mockFetch.mockResolvedValueOnce({ - ok: true, - json: () => - Promise.resolve({ - items: [mockAirtableTool], - limit: 100, - offset: 0, - total_count: 100, // Says 100 but only returns 1 - }), - }); + mockFetch.mockResolvedValueOnce( + asResponse({ + ok: true, + json: () => + Promise.resolve({ + items: [mockAirtableTool], + limit: 100, + offset: 0, + total_count: 100, // Says 100 but only returns 1 + }), + }) + ); - mockFetch.mockResolvedValueOnce({ - ok: true, - json: () => - Promise.resolve({ - items: [], - limit: 100, - offset: 1, - total_count: 100, - }), - }); + mockFetch.mockResolvedValueOnce( + asResponse({ + ok: true, + json: () => + Promise.resolve({ + items: [], + limit: 100, + offset: 1, + total_count: 100, + }), + }) + ); const source = new ArcadeApiSource({ baseUrl: "https://api.arcade.dev", @@ -558,11 +604,13 @@ describe("ArcadeApiSource", () => { describe("fetchToolsByToolkit", () => { it("should call fetchAllTools with toolkit filter", async () => { - mockFetch.mockResolvedValueOnce({ - ok: true, - json: () => - Promise.resolve(createMockArcadeResponse([mockGoogleCalendarTool])), - }); + mockFetch.mockResolvedValueOnce( + asResponse({ + ok: true, + json: () => + Promise.resolve(createMockArcadeResponse([mockGoogleCalendarTool])), + }) + ); const source = new ArcadeApiSource({ baseUrl: "https://api.arcade.dev", @@ -579,10 +627,12 @@ describe("ArcadeApiSource", () => { describe("isAvailable", () => { it("should return true when API is accessible", async () => { - mockFetch.mockResolvedValueOnce({ - ok: true, - json: () => Promise.resolve(createMockArcadeResponse([])), - }); + mockFetch.mockResolvedValueOnce( + asResponse({ + ok: true, + json: () => Promise.resolve(createMockArcadeResponse([])), + }) + ); const source = new ArcadeApiSource({ baseUrl: "https://api.arcade.dev", @@ -595,11 +645,13 @@ describe("ArcadeApiSource", () => { }); it("should return false when API returns error", async () => { - mockFetch.mockResolvedValueOnce({ - ok: false, - status: 401, - statusText: "Unauthorized", - }); + mockFetch.mockResolvedValueOnce( + asResponse({ + ok: false, + status: 401, + statusText: "Unauthorized", + }) + ); const source = new ArcadeApiSource({ baseUrl: "https://api.arcade.dev", @@ -627,13 +679,15 @@ describe("ArcadeApiSource", () => { describe("error handling", () => { it("should throw on API error with JSON detail", async () => { - mockFetch.mockResolvedValueOnce({ - ok: false, - status: 401, - statusText: "Unauthorized", - headers: new Map([["content-type", "application/json"]]), - json: () => Promise.resolve({ detail: "Invalid API key" }), - }); + mockFetch.mockResolvedValueOnce( + asResponse({ + ok: false, + status: 401, + statusText: "Unauthorized", + headers: new Map([["content-type", "application/json"]]), + json: () => Promise.resolve({ detail: "Invalid API key" }), + }) + ); const source = new ArcadeApiSource({ baseUrl: "https://api.arcade.dev", @@ -647,12 +701,14 @@ describe("ArcadeApiSource", () => { }); it("should throw on API error without JSON detail", async () => { - mockFetch.mockResolvedValueOnce({ - ok: false, - status: 500, - statusText: "Internal Server Error", - headers: new Map([["content-type", "text/plain"]]), - }); + mockFetch.mockResolvedValueOnce( + asResponse({ + ok: false, + status: 500, + statusText: "Internal Server Error", + headers: new Map([["content-type", "text/plain"]]), + }) + ); const source = new ArcadeApiSource({ baseUrl: "https://api.arcade.dev", @@ -666,10 +722,12 @@ describe("ArcadeApiSource", () => { }); it("should throw on invalid response schema", async () => { - mockFetch.mockResolvedValueOnce({ - ok: true, - json: () => Promise.resolve({ invalid: "response" }), - }); + mockFetch.mockResolvedValueOnce( + asResponse({ + ok: true, + json: () => Promise.resolve({ invalid: "response" }), + }) + ); const source = new ArcadeApiSource({ baseUrl: "https://api.arcade.dev", @@ -685,10 +743,12 @@ describe("ArcadeApiSource", () => { describe("authorization header", () => { it("should include Bearer token in request", async () => { - mockFetch.mockResolvedValueOnce({ - ok: true, - json: () => Promise.resolve(createMockArcadeResponse([])), - }); + mockFetch.mockResolvedValueOnce( + asResponse({ + ok: true, + json: () => Promise.resolve(createMockArcadeResponse([])), + }) + ); const source = new ArcadeApiSource({ baseUrl: "https://api.arcade.dev", diff --git a/toolkit-docs-generator/tests/sources/custom-sections-file.test.ts b/toolkit-docs-generator/tests/sources/custom-sections-file.test.ts index 1e4af1936..8923abf80 100644 --- a/toolkit-docs-generator/tests/sources/custom-sections-file.test.ts +++ b/toolkit-docs-generator/tests/sources/custom-sections-file.test.ts @@ -2,7 +2,7 @@ import { mkdtemp, rm, writeFile } from "fs/promises"; import { tmpdir } from "os"; import { join } from "path"; import { afterEach, describe, expect, it } from "vitest"; -import { createCustomSectionsFileSource } from "../../src/sources/custom-sections-file.js"; +import { createCustomSectionsFileSource } from "../../src/sources/custom-sections-file"; const createTempDir = async (): Promise => mkdtemp(join(tmpdir(), "custom-sections-")); diff --git a/toolkit-docs-generator/tests/sources/design-system-metadata.test.ts b/toolkit-docs-generator/tests/sources/design-system-metadata.test.ts index 32e0d7c6a..9f33b8f51 100644 --- a/toolkit-docs-generator/tests/sources/design-system-metadata.test.ts +++ b/toolkit-docs-generator/tests/sources/design-system-metadata.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from "vitest"; -import { createDesignSystemMetadataSourceFromToolkits } from "../../src/sources/design-system-metadata.js"; -import type { ToolkitMetadata } from "../../src/types/index.js"; +import { createDesignSystemMetadataSourceFromToolkits } from "../../src/sources/design-system-metadata"; +import type { ToolkitMetadata } from "../../src/types/index"; const createMetadata = ( overrides: Partial = {} diff --git a/toolkit-docs-generator/tests/sources/engine-api.test.ts b/toolkit-docs-generator/tests/sources/engine-api.test.ts index ebadf86a7..6056fbfcc 100644 --- a/toolkit-docs-generator/tests/sources/engine-api.test.ts +++ b/toolkit-docs-generator/tests/sources/engine-api.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from "vitest"; -import { EngineApiSource } from "../../src/sources/engine-api.js"; +import { EngineApiSource } from "../../src/sources/engine-api"; type ToolMetadataItem = { fully_qualified_name: string; @@ -39,7 +39,7 @@ type ToolMetadataItem = { provider_id: string | null; provider_type: string | null; scopes: string[]; - }>; + }> | null; secrets: Array<{ key: string }>; } | null; metadata?: { @@ -111,7 +111,7 @@ const createItems = (): ToolMetadataItem[] => [ const createFetchStub = (items: ToolMetadataItem[], status = 200) => - async (input: RequestInfo | URL) => { + async (input: string | URL | Request) => { if (status !== 200) { return new Response("error", { status }); } @@ -162,7 +162,7 @@ const createErrorFetchStub = (status: number, payload: unknown) => async () => const createInspectFetchStub = (inspect: (params: URLSearchParams) => void) => - async (input: RequestInfo | URL) => { + async (input: string | URL | Request) => { const url = new URL(input.toString()); inspect(url.searchParams); return new Response( @@ -179,7 +179,7 @@ const createInspectFetchStub = const createSummaryFetchStub = (payload: unknown, inspect?: (url: URL) => void) => - async (input: RequestInfo | URL) => { + async (input: string | URL | Request) => { const url = new URL(input.toString()); inspect?.(url); return new Response(JSON.stringify(payload), { @@ -228,7 +228,7 @@ describe("EngineApiSource", () => { description: "GitHub toolkit", }, input: { parameters: [] }, - output: {} as ToolMetadataItem["output"], + output: {} as NonNullable, requirements: { authorization: null, secrets: [], diff --git a/toolkit-docs-generator/tests/sources/in-memory.test.ts b/toolkit-docs-generator/tests/sources/in-memory.test.ts index 917b4a93b..aca13dcd5 100644 --- a/toolkit-docs-generator/tests/sources/in-memory.test.ts +++ b/toolkit-docs-generator/tests/sources/in-memory.test.ts @@ -12,12 +12,12 @@ import { InMemoryCustomSectionsSource, InMemoryMetadataSource, InMemoryToolDataSource, -} from "../../src/sources/in-memory.js"; +} from "../../src/sources/in-memory"; import type { CustomSections, ToolDefinition, ToolkitMetadata, -} from "../../src/types/index.js"; +} from "../../src/types/index"; // ============================================================================ // Test Fixtures - Realistic data matching production schema diff --git a/toolkit-docs-generator/tests/sources/oauth-provider-resolver.test.ts b/toolkit-docs-generator/tests/sources/oauth-provider-resolver.test.ts index d25695e55..98a7c8b99 100644 --- a/toolkit-docs-generator/tests/sources/oauth-provider-resolver.test.ts +++ b/toolkit-docs-generator/tests/sources/oauth-provider-resolver.test.ts @@ -10,7 +10,7 @@ import { describe, expect, it } from "vitest"; import { buildProviderIdResolver, normalizeLookup, -} from "../../src/sources/oauth-provider-resolver.js"; +} from "../../src/sources/oauth-provider-resolver"; // ============================================================================ // Fixtures diff --git a/toolkit-docs-generator/tests/sources/toolkit-data-source.test.ts b/toolkit-docs-generator/tests/sources/toolkit-data-source.test.ts index 99d93ee31..4d3f3fdd5 100644 --- a/toolkit-docs-generator/tests/sources/toolkit-data-source.test.ts +++ b/toolkit-docs-generator/tests/sources/toolkit-data-source.test.ts @@ -5,18 +5,18 @@ * abstraction returns tools + metadata together. */ import { describe, expect, it } from "vitest"; -import { createDesignSystemMetadataSourceFromToolkits } from "../../src/sources/design-system-metadata.js"; +import { createDesignSystemMetadataSourceFromToolkits } from "../../src/sources/design-system-metadata"; import { InMemoryMetadataSource, InMemoryToolDataSource, -} from "../../src/sources/in-memory.js"; -import type { IMetadataSource } from "../../src/sources/internal.js"; +} from "../../src/sources/in-memory"; +import type { IMetadataSource } from "../../src/sources/internal"; import { createCachedToolkitDataSource, createCombinedToolkitDataSource, type IToolkitDataSource, -} from "../../src/sources/toolkit-data-source.js"; -import type { ToolDefinition, ToolkitMetadata } from "../../src/types/index.js"; +} from "../../src/sources/toolkit-data-source"; +import type { ToolDefinition, ToolkitMetadata } from "../../src/types/index"; const createTool = ( overrides: Partial = {} diff --git a/toolkit-docs-generator/tests/utils/excluded-output-cleanup.test.ts b/toolkit-docs-generator/tests/utils/excluded-output-cleanup.test.ts index 89ee2865d..647a48cd7 100644 --- a/toolkit-docs-generator/tests/utils/excluded-output-cleanup.test.ts +++ b/toolkit-docs-generator/tests/utils/excluded-output-cleanup.test.ts @@ -2,7 +2,7 @@ import { mkdtemp, readdir, rm, writeFile } from "fs/promises"; import { tmpdir } from "os"; import { join } from "path"; import { afterEach, describe, expect, it } from "vitest"; -import { removeExcludedToolkitFiles } from "../../src/utils/excluded-output-cleanup.js"; +import { removeExcludedToolkitFiles } from "../../src/utils/excluded-output-cleanup"; let tmpDir: string; diff --git a/toolkit-docs-generator/tests/utils/exclusion-list.test.ts b/toolkit-docs-generator/tests/utils/exclusion-list.test.ts index 9bbb5f2d9..f4622023c 100644 --- a/toolkit-docs-generator/tests/utils/exclusion-list.test.ts +++ b/toolkit-docs-generator/tests/utils/exclusion-list.test.ts @@ -2,7 +2,7 @@ import { mkdtemp, rm, writeFile } from "fs/promises"; import { tmpdir } from "os"; import { join } from "path"; import { afterEach, describe, expect, it } from "vitest"; -import { readExclusionList } from "../../src/utils/exclusion-list.js"; +import { readExclusionList } from "../../src/utils/exclusion-list"; let tmpDir: string; diff --git a/toolkit-docs-generator/tests/utils/ignore-list.test.ts b/toolkit-docs-generator/tests/utils/ignore-list.test.ts index 293aafaee..c27f2e58f 100644 --- a/toolkit-docs-generator/tests/utils/ignore-list.test.ts +++ b/toolkit-docs-generator/tests/utils/ignore-list.test.ts @@ -2,7 +2,7 @@ import { mkdtemp, rm, writeFile } from "fs/promises"; import { tmpdir } from "os"; import { join } from "path"; import { afterEach, describe, expect, it } from "vitest"; -import { readIgnoreList } from "../../src/utils/ignore-list.js"; +import { readIgnoreList } from "../../src/utils/ignore-list"; let tmpDir: string; diff --git a/toolkit-docs-generator/tests/utils/output-dir.test.ts b/toolkit-docs-generator/tests/utils/output-dir.test.ts index 278cda2f9..f7524a21e 100644 --- a/toolkit-docs-generator/tests/utils/output-dir.test.ts +++ b/toolkit-docs-generator/tests/utils/output-dir.test.ts @@ -6,13 +6,31 @@ import { clearSafeOutputDir, resolveDefaultOutputDir, resolveSafeOutputDir, -} from "../../src/utils/output-dir.js"; +} from "../../src/utils/output-dir"; + +type ResolveOptions = { repoRoot?: string; homeDir?: string }; describe("resolveSafeOutputDir", () => { const originalCwd = process.cwd(); let repoRoot: string | null = null; let homeDir: string | null = null; + /** + * `repoRoot`/`homeDir` are optional properties without `exactOptionalPropertyTypes` + * allowing an explicit `undefined` value, so this only sets a key when the + * corresponding temp dir has actually been created for the current test. + */ + const dirOptions = (): ResolveOptions => { + const opts: ResolveOptions = {}; + if (repoRoot) { + opts.repoRoot = repoRoot; + } + if (homeDir) { + opts.homeDir = homeDir; + } + return opts; + }; + beforeEach(async () => { repoRoot = await mkdtemp(join(tmpdir(), "generator-repo-")); homeDir = await mkdtemp(join(tmpdir(), "generator-home-")); @@ -35,8 +53,7 @@ describe("resolveSafeOutputDir", () => { await mkdir(join(repoRoot ?? "", "output"), { recursive: true }); const resolved = await resolveSafeOutputDir("output", { - repoRoot: repoRoot ?? undefined, - homeDir: homeDir ?? undefined, + ...dirOptions(), }); const expected = await realpath(join(repoRoot ?? "", "output")); @@ -46,8 +63,7 @@ describe("resolveSafeOutputDir", () => { it("rejects relative paths that escape the repo root", async () => { await expect( resolveSafeOutputDir("../outside", { - repoRoot: repoRoot ?? undefined, - homeDir: homeDir ?? undefined, + ...dirOptions(), }) ).rejects.toThrow("outside repo root"); }); @@ -55,8 +71,7 @@ describe("resolveSafeOutputDir", () => { it("rejects deleting the filesystem root", async () => { await expect( resolveSafeOutputDir("/", { - repoRoot: repoRoot ?? undefined, - homeDir: homeDir ?? undefined, + ...dirOptions(), }) ).rejects.toThrow("unsafe output directory"); }); @@ -64,8 +79,7 @@ describe("resolveSafeOutputDir", () => { it("rejects deleting the home directory", async () => { await expect( resolveSafeOutputDir(homeDir ?? "", { - repoRoot: repoRoot ?? undefined, - homeDir: homeDir ?? undefined, + ...dirOptions(), }) ).rejects.toThrow("unsafe output directory"); }); @@ -101,8 +115,7 @@ describe("resolveSafeOutputDir", () => { const expected = await realpath(outputDir); const cleared = await clearSafeOutputDir(outputDir, { - repoRoot: repoRoot ?? undefined, - homeDir: homeDir ?? undefined, + ...dirOptions(), }); expect(cleared).toBe(expected); @@ -116,8 +129,7 @@ describe("resolveSafeOutputDir", () => { try { await expect( clearSafeOutputDir(`../${outsideName}`, { - repoRoot: repoRoot ?? undefined, - homeDir: homeDir ?? undefined, + ...dirOptions(), }) ).rejects.toThrow("outside repo root"); expect(await realpath(outsideDir)).toContain(outsideName); diff --git a/toolkit-docs-generator/tests/utils/progress.test.ts b/toolkit-docs-generator/tests/utils/progress.test.ts index bc3dee7bd..18f653e43 100644 --- a/toolkit-docs-generator/tests/utils/progress.test.ts +++ b/toolkit-docs-generator/tests/utils/progress.test.ts @@ -4,7 +4,7 @@ import { formatDuration, formatToolkitComplete, formatToolkitError, -} from "../../src/utils/progress.js"; +} from "../../src/utils/progress"; describe("formatDuration", () => { it("formats milliseconds", () => { diff --git a/toolkit-docs-generator/tests/utils/provider-matching.test.ts b/toolkit-docs-generator/tests/utils/provider-matching.test.ts index 49967b31d..d3edcb040 100644 --- a/toolkit-docs-generator/tests/utils/provider-matching.test.ts +++ b/toolkit-docs-generator/tests/utils/provider-matching.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from "vitest"; -import type { ProviderVersion } from "../../src/types/index.js"; -import { resolveProviderIdsFromMetadata } from "../../src/utils/provider-matching.js"; +import type { ProviderVersion } from "../../src/types/index"; +import { resolveProviderIdsFromMetadata } from "../../src/utils/provider-matching"; describe("resolveProviderIdsFromMetadata", () => { it("matches by toolkit id (case-insensitive)", () => { diff --git a/toolkit-docs-generator/tests/utils/retry.test.ts b/toolkit-docs-generator/tests/utils/retry.test.ts index bdb59c470..f2b6b6a9c 100644 --- a/toolkit-docs-generator/tests/utils/retry.test.ts +++ b/toolkit-docs-generator/tests/utils/retry.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it, vi } from "vitest"; -import { createRetryWrapper, withRetry } from "../../src/utils/retry.js"; +import { createRetryWrapper, withRetry } from "../../src/utils/retry"; describe("withRetry", () => { it("should succeed on first attempt if no error", async () => { diff --git a/toolkit-docs-generator/tests/utils/run-logs.test.ts b/toolkit-docs-generator/tests/utils/run-logs.test.ts index 8539ba8e8..3b5499de9 100644 --- a/toolkit-docs-generator/tests/utils/run-logs.test.ts +++ b/toolkit-docs-generator/tests/utils/run-logs.test.ts @@ -6,7 +6,7 @@ import { describe, expect, it } from "vitest"; import { readFailedToolsReport, writeFailedToolsReport, -} from "../../src/utils/run-logs.js"; +} from "../../src/utils/run-logs"; const withTempDir = async (fn: (dir: string) => Promise) => { const dir = await mkdtemp(join(tmpdir(), "toolkit-logs-")); diff --git a/toolkit-docs-generator/tests/utils/version-coherence.test.ts b/toolkit-docs-generator/tests/utils/version-coherence.test.ts index dfcf9a762..c942eae33 100644 --- a/toolkit-docs-generator/tests/utils/version-coherence.test.ts +++ b/toolkit-docs-generator/tests/utils/version-coherence.test.ts @@ -2,11 +2,11 @@ * Tests for highest-version coherence filter */ import { describe, expect, it } from "vitest"; -import type { ToolDefinition } from "../../src/types/index.js"; +import type { ToolDefinition } from "../../src/types/index"; import { filterToolsByHighestVersion, getHighestVersion, -} from "../../src/utils/version-coherence.js"; +} from "../../src/utils/version-coherence"; const createTool = ( fullyQualifiedName: string, diff --git a/toolkit-docs-generator/tests/workflows/generate-toolkit-docs.test.ts b/toolkit-docs-generator/tests/workflows/generate-toolkit-docs.test.ts index 4dbaff794..94990321d 100644 --- a/toolkit-docs-generator/tests/workflows/generate-toolkit-docs.test.ts +++ b/toolkit-docs-generator/tests/workflows/generate-toolkit-docs.test.ts @@ -20,9 +20,14 @@ test("porter workflow includes required triggers", () => { }); test("porter workflow generates docs and opens a PR", () => { - expect(workflowContents).toContain("pnpm dlx tsx src/cli/index.ts generate"); + expect(workflowContents).toContain( + "../node_modules/.bin/tsx src/cli/index.ts generate" + ); + // pnpm dlx resolves an unpinned tsx from the registry on every run, so the + // nightly's TypeScript runtime would drift outside the lockfile. + expect(workflowContents).not.toContain("pnpm dlx"); expect(workflowContents).toContain("--skip-unchanged"); - expect(workflowContents).toContain("--require-complete"); + expect(workflowContents).toContain("--preserve-last-known-good"); expect(workflowContents).toContain("--verbose"); expect(workflowContents).toContain("--api-source tool-metadata"); expect(workflowContents).toContain("--tool-metadata-url"); @@ -74,6 +79,13 @@ test("porter workflow alerts Slack when generation fails", () => { expect(workflowContents).not.toContain("\\\\n"); }); +test("porter workflow warns when it preserves or omits a broken toolkit", () => { + expect(workflowContents).toContain("preservedToolkits"); + expect(workflowContents).toContain("omittedToolkits"); + expect(workflowContents).toContain("Continuing to serve previous docs"); + expect(workflowContents).toContain("No docs are being served"); +}); + test("workflow dispatch keeps default full-run behavior", () => { expect(workflowContents).toContain("workflow_dispatch:"); expect(workflowContents).toContain("--all"); diff --git a/toolkit-docs-generator/tsconfig.json b/toolkit-docs-generator/tsconfig.json index 8acdb3fea..1d47aaddd 100644 --- a/toolkit-docs-generator/tsconfig.json +++ b/toolkit-docs-generator/tsconfig.json @@ -1,18 +1,14 @@ { "compilerOptions": { "target": "ES2022", - "module": "NodeNext", - "moduleResolution": "NodeNext", + "module": "esnext", + "moduleResolution": "bundler", "lib": ["ES2022"], "strict": true, "esModuleInterop": true, "skipLibCheck": true, "forceConsistentCasingInFileNames": true, - "declaration": true, - "declarationMap": true, - "sourceMap": true, - "outDir": "./dist", - "rootDir": "./src", + "noEmit": true, "resolveJsonModule": true, "noUnusedLocals": true, "noUnusedParameters": true, @@ -21,6 +17,10 @@ "exactOptionalPropertyTypes": true, "noUncheckedIndexedAccess": true }, - "include": ["src/**/*"], - "exclude": ["node_modules", "dist", "**/*.test.ts"] + "include": ["src/**/*", "scripts/**/*", "tests/**/*"], + "exclude": [ + "node_modules", + "dist", + "tests/scripts/sync-toolkit-sidebar.test.ts" + ] } diff --git a/toolkit-docs-generator/vitest.config.ts b/toolkit-docs-generator/vitest.config.ts deleted file mode 100644 index 3ff060c9f..000000000 --- a/toolkit-docs-generator/vitest.config.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { fileURLToPath } from "node:url"; -import { defineConfig } from "vitest/config"; - -export default defineConfig({ - root: fileURLToPath(new URL(".", import.meta.url)), - test: { - // Enable globals like describe, it, expect without imports - globals: true, - - // Test environment - environment: "node", - - // Include test files - include: ["tests/**/*.test.ts"], - - // Coverage configuration - coverage: { - provider: "v8", - reporter: ["text", "json", "html"], - exclude: [ - "node_modules/", - "dist/", - "tests/", - "**/*.d.ts", - "vitest.config.ts", - ], - // Require 80% coverage - thresholds: { - lines: 80, - functions: 80, - branches: 80, - statements: 80, - }, - }, - - // TypeScript configuration - typecheck: { - enabled: true, - }, - }, -}); diff --git a/tsconfig.json b/tsconfig.json index ca0fc204d..1ebdde928 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -23,11 +23,16 @@ }, "strictNullChecks": true }, + "files": [ + "toolkit-docs-generator/tests/scripts/sync-toolkit-sidebar.test.ts" + ], "include": [ "next-env.d.ts", "app/**/*.ts", "app/**/*.tsx", "lib/**/*.ts", + "tests/**/*.ts", + "tests/**/*.tsx", "_dictionaries/**/*.ts", ".next/types/**/*.ts", "app/[lang]/page.mdx",