diff --git a/package.json b/package.json index 0075499f8..18adf1e7e 100644 --- a/package.json +++ b/package.json @@ -155,7 +155,7 @@ "test:system-messages:vitest": "pnpm exec vitest run test/systemMessages.test.ts", "test:tool-routing-semantic:vitest": "pnpm exec vitest run test/toolRoutingSemantic.test.ts", "test:tool-routing-semantic": "pnpm run test:tool-routing-semantic:vitest && npx tsx test/continuous-test-suite-tool-routing-semantic.ts", - "test:unit": "pnpm run test:envguard && pnpm run test:bugfixes && pnpm run test:file-detector-extension && pnpm run test:file-detector-magic-bytes && pnpm run test:mcp:infra && pnpm run test:mcp:bash && pnpm run test:mcp:limits && pnpm run test:mcp:spans && pnpm run test:autoresearch:redis && pnpm run test:unit:vitest && pnpm run test:tool-routing-cli:vitest && pnpm run test:tool-dedup:vitest && pnpm run test:model-pool:vitest && pnpm run test:litellm-context:vitest && pnpm run test:step-budget-guard:vitest && pnpm run test:system-messages:vitest && pnpm run test:tool-routing-semantic:vitest && pnpm run test:anthropic-tools-policy && pnpm run test:sagemaker-tools && pnpm run test:anthropic-multimodal && pnpm run test:excel-interop && pnpm run test:model-capabilities:vitest && pnpm run test:agent-runtime:vitest && pnpm run test:retry-after:vitest", + "test:unit": "pnpm run test:envguard && pnpm run test:bugfixes && pnpm run test:file-detector-extension && pnpm run test:file-detector-magic-bytes && pnpm run test:mcp:infra && pnpm run test:mcp:bash && pnpm run test:mcp:limits && pnpm run test:mcp:spans && pnpm run test:autoresearch:redis && pnpm run test:unit:vitest && pnpm run test:tool-routing-cli:vitest && pnpm run test:tool-dedup:vitest && pnpm run test:model-pool:vitest && pnpm run test:litellm-context:vitest && pnpm run test:step-budget-guard:vitest && pnpm run test:system-messages:vitest && pnpm run test:tool-routing-semantic:vitest && pnpm run test:anthropic-tools-policy && pnpm run test:sagemaker-tools && pnpm run test:anthropic-multimodal && pnpm run test:excel-interop && pnpm run test:model-capabilities:vitest && pnpm run test:agent-runtime:vitest && pnpm run test:retry-after:vitest && pnpm run test:proxy:vitest && pnpm run test:knowledge-grounding:vitest && pnpm run test:mcp-result-cache:vitest && pnpm run test:prompt-redaction:vitest", "// CI tier — live providers, runs only when API keys are present (test:credentials and test:dynamic make real provider calls when keys are set, so they live here, not in test:unit)": "", "test:live": "pnpm run test:providers && pnpm run test:mcp:http && pnpm run test:mcp:sdk && pnpm run test:mcp:cli && pnpm run test:observability && pnpm run test:context && pnpm run test:memory && pnpm run test:tool-reliability && pnpm run test:evaluation && pnpm run test:autoresearch && pnpm run test:credentials && pnpm run test:dynamic", "// CI tier — product output (image/video/TTS/PPT) — costs $$ per run": "", @@ -218,7 +218,11 @@ "pre-push": "pnpm run validate:commit && pnpm run validate:env && pnpm run validate && pnpm run test:ci", "check:all": "pnpm run lint && pnpm run format --check && pnpm run validate && pnpm run validate:commit", "test:litellm-context:vitest": "pnpm exec vitest run test/litellmContextWindows.test.ts", - "test:step-budget-guard:vitest": "pnpm exec vitest run test/stepBudgetGuard.test.ts" + "test:step-budget-guard:vitest": "pnpm exec vitest run test/stepBudgetGuard.test.ts", + "test:proxy:vitest": "pnpm exec vitest run test/proxyAnalysis.test.ts test/proxyConfigHotReload.test.ts test/proxyObservabilityFoundation.test.ts test/proxyReliabilityHardening.test.ts test/proxyReplay.test.ts test/proxyRollingWorkerHandoff.test.ts test/proxyTerminalErrorAccounting.test.ts test/proxyUpdaterFallback.test.ts test/proxyUsageStatsPersistence.test.ts", + "test:knowledge-grounding:vitest": "pnpm exec vitest run test/knowledgeGrounding.test.ts", + "test:mcp-result-cache:vitest": "pnpm exec vitest run test/mcpResultCacheErrorSkip.test.ts", + "test:prompt-redaction:vitest": "pnpm exec vitest run test/promptRedaction.test.ts" }, "files": [ "dist", diff --git a/scripts/build-validations.ts b/scripts/build-validations.ts index 754c45fc4..870e32cd6 100644 --- a/scripts/build-validations.ts +++ b/scripts/build-validations.ts @@ -505,6 +505,51 @@ class NeuroLinkBuildValidator { this.log("Project structure validation completed"); } + // Every test/*.test.ts must be reachable from a package.json script. + // + // A Vitest file that no script names is never executed by anything: not + // `pnpm test`, not `test:unit`, not a developer following the docs. It looks + // like coverage in review and silently rots — twelve files had drifted into + // that state, one of them holding a genuine failing assertion nobody had seen. + checkOrphanedTestFiles(): void { + this.log("Validating that every test file is reachable from a script..."); + + const testDir = path.join(this.rootDir, "test"); + if (!fs.existsSync(testDir)) { + this.warnings.push("No test/ directory found; skipping orphan check"); + return; + } + + const packageJson = this.readFileWithCache( + path.join(this.rootDir, "package.json"), + ); + if (!packageJson) { + this.errors.push("Could not read package.json to check test wiring"); + return; + } + // Match against the scripts block alone: a filename appearing in some other + // field would otherwise count as "wired" without running anything. + const scripts = JSON.stringify( + (JSON.parse(packageJson) as { scripts?: Record }) + .scripts ?? {}, + ); + + const orphans = fs + .readdirSync(testDir) + .filter((name) => name.endsWith(".test.ts")) + .filter((name) => !scripts.includes(name)); + + if (orphans.length > 0) { + this.errors.push( + `${orphans.length} test file(s) are not referenced by any package.json script, ` + + `so nothing runs them: ${orphans.join(", ")}. ` + + `Add each to a "test::vitest" script and chain it into "test:unit".`, + ); + } + + this.log(`Test wiring check completed (${orphans.length} orphaned)`); + } + // Main validation runner run(): void { console.log("Running NeuroLink Build Validations...\n"); @@ -520,6 +565,7 @@ class NeuroLinkBuildValidator { this.checkErrorHandling(); this.checkTodoReferences(); this.checkEnvironmentConfig(); + this.checkOrphanedTestFiles(); const endTime = Date.now(); const duration = ((endTime - startTime) / 1000).toFixed(2); diff --git a/test/proxyTerminalErrorAccounting.test.ts b/test/proxyTerminalErrorAccounting.test.ts index 579af5f41..c772eeb07 100644 --- a/test/proxyTerminalErrorAccounting.test.ts +++ b/test/proxyTerminalErrorAccounting.test.ts @@ -264,6 +264,12 @@ describe("translated terminal accounting", () => { const modelRouter = { resolve: () => ({ provider: "openai", model: "translated-model" }), getFallbackChain: () => [], + // Auto-fallback became opt-in in "fix(proxy): bound account admission and + // fallback routing" (33bdd141). This test is about the SECOND attempt not + // finalizing the terminal error a second time, so it has to opt in — a + // router without this reports one attempt and never reaches the path + // under test. Default-off behaviour is pinned by the test below. + isAutoFallbackEnabled: () => true, }; const messagesRoute = createClaudeProxyRoutes( modelRouter as never, @@ -278,6 +284,7 @@ describe("translated terminal accounting", () => { type: "error", }); + // Two attempts, but the terminal error is finalized once. expect(getStats()).toMatchObject({ totalAttempts: 2, totalAttemptErrors: 2, @@ -298,6 +305,51 @@ describe("translated terminal accounting", () => { }); }); + it("makes no second attempt when auto-fallback is left disabled", async () => { + // The default since 33bdd141: without an explicit opt-in, the translation + // layer must not pick a fallback provider on its own. This is the behaviour + // that silently changed the test above from passing to failing while the + // file was orphaned, so it gets pinned rather than left implicit. + await startRequestLogCapture(); + const ctx = createContext("translated-route-no-autofallback", async () => ({ + stream: (async function* () { + yield* []; + })(), + toolCalls: [], + usage: {}, + model: "translated-model", + })) as ServerContext & { body: Record }; + ctx.body = { + model: "claude-routed-model", + messages: [{ role: "user", content: "hello" }], + }; + const modelRouter = { + resolve: () => ({ provider: "openai", model: "translated-model" }), + getFallbackChain: () => [], + isAutoFallbackEnabled: () => false, + }; + const messagesRoute = createClaudeProxyRoutes( + modelRouter as never, + ).routes.find( + (route) => route.method === "POST" && route.path === "/v1/messages", + ); + if (!messagesRoute) { + throw new Error("messages route not found"); + } + + await expect(messagesRoute.handler(ctx)).resolves.toMatchObject({ + type: "error", + }); + + // One attempt, not two — and still exactly one finalized terminal error. + expect(getStats()).toMatchObject({ + totalAttempts: 1, + totalAttemptErrors: 1, + totalRequests: 1, + totalErrors: 1, + }); + }); + it("records client cancellation once without marking the stream successful", async () => { const logDir = await startRequestLogCapture(); let resolvePendingRead!: (value: IteratorResult) => void;