Просмотр исходного кода

Merge pull request #5440 from Kilo-Org/mark/redirect-loops

Remove redirect loops
Mark IJbema 2 недель назад
Родитель
Сommit
f707fcc8bf

+ 137 - 0
apps/kilocode-docs/__tests__/previous-docs-redirects.spec.ts

@@ -0,0 +1,137 @@
+/**
+ * Tests for redirect loop detection in previous-docs-redirects.js
+ *
+ * This test suite verifies that the redirect configuration has no loops:
+ * 1. Direct loops: A path redirecting to itself (source === destination)
+ * 2. Indirect loops: A chain of redirects leading back to a starting point (A → B → C → A)
+ */
+
+import { expect, describe, it } from "vitest"
+import redirects from "../previous-docs-redirects.js"
+
+interface Redirect {
+	source: string
+	destination: string
+	basePath?: boolean
+	permanent?: boolean
+}
+
+describe("previous-docs-redirects", () => {
+	describe("direct loop detection", () => {
+		it("should not have any redirects where source equals destination", () => {
+			const directLoops: Redirect[] = []
+
+			for (const redirect of redirects as Redirect[]) {
+				if (redirect.source === redirect.destination) {
+					directLoops.push(redirect)
+				}
+			}
+
+			if (directLoops.length > 0) {
+				const loopDetails = directLoops.map((r) => `  - "${r.source}" redirects to itself`).join("\n")
+				expect.fail(`Found ${directLoops.length} direct redirect loop(s):\n${loopDetails}`)
+			}
+		})
+	})
+
+	describe("indirect loop detection", () => {
+		it("should not have any redirect chains that form a cycle", () => {
+			// Build a map of source -> destination for quick lookup
+			// Note: We only consider exact path matches, not wildcard patterns like :path*
+			// Also skip direct loops (source === destination) as they're caught by the direct loop test
+			const redirectMap = new Map<string, string>()
+
+			for (const redirect of redirects as Redirect[]) {
+				// Skip wildcard redirects as they don't form exact chains
+				// Skip direct loops as they're caught by the direct loop test
+				if (
+					!redirect.source.includes(":") &&
+					!redirect.source.includes("*") &&
+					redirect.source !== redirect.destination
+				) {
+					redirectMap.set(redirect.source, redirect.destination)
+				}
+			}
+
+			const cycles: string[][] = []
+
+			/**
+			 * Detects if following redirects from a starting path leads back to any path in the chain.
+			 * Uses a visited set to track the current chain and detect cycles.
+			 */
+			function detectCycle(startPath: string): string[] | null {
+				const visited = new Set<string>()
+				const chain: string[] = [startPath]
+				let currentPath = startPath
+
+				while (redirectMap.has(currentPath)) {
+					const nextPath = redirectMap.get(currentPath)!
+
+					if (visited.has(nextPath)) {
+						// Found a cycle - return the chain from the cycle start
+						const cycleStartIndex = chain.indexOf(nextPath)
+						if (cycleStartIndex !== -1) {
+							return [...chain.slice(cycleStartIndex), nextPath]
+						}
+						return null
+					}
+
+					visited.add(currentPath)
+					chain.push(nextPath)
+					currentPath = nextPath
+				}
+
+				return null
+			}
+
+			// Check each redirect source for potential cycles
+			for (const source of redirectMap.keys()) {
+				const cycle = detectCycle(source)
+				if (cycle) {
+					// Avoid duplicate cycle reports by checking if we've already found this cycle
+					const cycleKey = [...cycle].sort().join(" -> ")
+					const isDuplicate = cycles.some(
+						(existingCycle) => [...existingCycle].sort().join(" -> ") === cycleKey,
+					)
+					if (!isDuplicate) {
+						cycles.push(cycle)
+					}
+				}
+			}
+
+			if (cycles.length > 0) {
+				const cycleDetails = cycles.map((cycle) => `  - ${cycle.join(" → ")}`).join("\n")
+				expect.fail(`Found ${cycles.length} indirect redirect cycle(s):\n${cycleDetails}`)
+			}
+		})
+	})
+
+	describe("redirect structure validation", () => {
+		it("should have valid redirect objects with required properties", () => {
+			const invalidRedirects: { index: number; issues: string[] }[] = []
+
+			;(redirects as Redirect[]).forEach((redirect, index) => {
+				const issues: string[] = []
+
+				if (typeof redirect.source !== "string" || redirect.source.trim() === "") {
+					issues.push("missing or invalid 'source' property")
+				}
+
+				if (typeof redirect.destination !== "string" || redirect.destination.trim() === "") {
+					issues.push("missing or invalid 'destination' property")
+				}
+
+				if (issues.length > 0) {
+					invalidRedirects.push({ index, issues })
+				}
+			})
+
+			if (invalidRedirects.length > 0) {
+				const details = invalidRedirects
+					.map((r) => `  - Redirect at index ${r.index}: ${r.issues.join(", ")}`)
+					.join("\n")
+				expect.fail(`Found ${invalidRedirects.length} invalid redirect(s):\n${details}`)
+			}
+		})
+	})
+})

+ 4 - 2
apps/kilocode-docs/package.json

@@ -5,7 +5,8 @@
 	"scripts": {
 		"dev": "next dev --webpack --port 3002",
 		"build": "next build --webpack",
-		"start": "next start"
+		"start": "next start",
+		"test": "vitest run"
 	},
 	"dependencies": {
 		"@docsearch/css": "^4",
@@ -27,6 +28,7 @@
 		"autoprefixer": "^10.4.23",
 		"postcss": "^8.5.6",
 		"tailwindcss": "^4.1.18",
-		"typescript": "^5.9.3"
+		"typescript": "^5.9.3",
+		"vitest": "^3.2.3"
 	}
 }

+ 0 - 60
apps/kilocode-docs/previous-docs-redirects.js

@@ -2,12 +2,6 @@ module.exports = [
 	// ============================================
 	// GET STARTED
 	// ============================================
-	{
-		source: "/docs/getting-started/quickstart",
-		destination: "/docs/getting-started/quickstart",
-		basePath: false,
-		permanent: true,
-	},
 	{
 		source: "/docs/getting-started/setting-up",
 		destination: "/docs/getting-started/setup-authentication",
@@ -554,82 +548,28 @@ module.exports = [
 	// ============================================
 	// CONTRIBUTING
 	// ============================================
-	{
-		source: "/docs/contributing",
-		destination: "/docs/contributing",
-		basePath: false,
-		permanent: true,
-	},
 	{
 		source: "/docs/contributing/index",
 		destination: "/docs/contributing",
 		basePath: false,
 		permanent: true,
 	},
-	{
-		source: "/docs/contributing/development-environment",
-		destination: "/docs/contributing/development-environment",
-		basePath: false,
-		permanent: true,
-	},
 
 	// ============================================
 	// CONTRIBUTING - Architecture
 	// ============================================
-	{
-		source: "/docs/contributing/architecture",
-		destination: "/docs/contributing/architecture",
-		basePath: false,
-		permanent: true,
-	},
 	{
 		source: "/docs/contributing/architecture/index",
 		destination: "/docs/contributing/architecture",
 		basePath: false,
 		permanent: true,
 	},
-	{
-		source: "/docs/contributing/architecture/annual-billing",
-		destination: "/docs/contributing/architecture/annual-billing",
-		basePath: false,
-		permanent: true,
-	},
-	{
-		source: "/docs/contributing/architecture/enterprise-mcp-controls",
-		destination: "/docs/contributing/architecture/enterprise-mcp-controls",
-		basePath: false,
-		permanent: true,
-	},
 	{
 		source: "/docs/contributing/architecture/onboarding-engagement-improvements",
 		destination: "/docs/contributing/architecture/onboarding-improvements",
 		basePath: false,
 		permanent: true,
 	},
-	{
-		source: "/docs/contributing/architecture/organization-modes-library",
-		destination: "/docs/contributing/architecture/organization-modes-library",
-		basePath: false,
-		permanent: true,
-	},
-	{
-		source: "/docs/contributing/architecture/track-repo-url",
-		destination: "/docs/contributing/architecture/track-repo-url",
-		basePath: false,
-		permanent: true,
-	},
-	{
-		source: "/docs/contributing/architecture/vercel-ai-gateway",
-		destination: "/docs/contributing/architecture/vercel-ai-gateway",
-		basePath: false,
-		permanent: true,
-	},
-	{
-		source: "/docs/contributing/architecture/voice-transcription",
-		destination: "/docs/contributing/architecture/voice-transcription",
-		basePath: false,
-		permanent: true,
-	},
 
 	// ============================================
 	// PAGES TO CONDENSE (Redirects to parent pages)

+ 9 - 0
apps/kilocode-docs/vitest.config.ts

@@ -0,0 +1,9 @@
+import { defineConfig } from "vitest/config"
+
+export default defineConfig({
+	test: {
+		globals: true,
+		watch: false,
+		testTimeout: 10_000,
+	},
+})

Разница между файлами не показана из-за своего большого размера
+ 153 - 150
pnpm-lock.yaml


Некоторые файлы не были показаны из-за большого количества измененных файлов