chore(qa): Final Wave cleanup — resolve lint findings from F2 review
Production code (all findings cleared):
- GenerationHistory: drop unused fetchProHistory(userId) param,
change let activeId to const
- InitiativeTracker: remove write-only currentSessionName state
- NotesPanel: remove unused isLoading flag
- Open5eReference: drop unused getCachedTranslation/setCachedTranslation
imports; use fetchTranslation result directly instead of re-lookup
- ai/openrouter.ts: drop unused z + Monster imports; attach cause to
rethrown AbortError
- ai/kimi.ts: drop unused z import
- api/dm/ai/generate.ts: annotate best-effort counter increment catch
- api/dm/ai/history.ts: drop unused sql import
Test files (trivial fixes; ~70 mock-related `as any` errors remain as
pre-existing tech debt across api test mocks):
- translation-client: drop unused MockBroadcastChannel _name param and
unused getCachedTranslation destructure
- open5e-types: eslint-disable for the intentional type-test aliases
- {notes,initiative}-api: remove unused mockUser2 placeholder
- {notes,initiative,translate,history}-api: let mockDb -> const mockDb
- ai-generate-api: wrap deliberate `var` mock decls in eslint-disable
with comment explaining vi.mock hoisting requirement
Verification: tsc 0 errors, vitest 339/339, lint 81 -> 70 errors
(production code fully clean; remaining errors are test-mock as-any
patterns from Waves 5-6).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -251,7 +251,7 @@ const { tier, userId } = Astro.props;
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function fetchProHistory(userId: number): Promise<HistoryItem[]> {
|
async function fetchProHistory(): Promise<HistoryItem[]> {
|
||||||
const response = await fetch(`/api/dm/ai/history?limit=${MAX_HISTORY}`);
|
const response = await fetch(`/api/dm/ai/history?limit=${MAX_HISTORY}`);
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
if (response.status === 401) {
|
if (response.status === 401) {
|
||||||
@@ -269,7 +269,7 @@ const { tier, userId } = Astro.props;
|
|||||||
const loadingEl = container.querySelector<HTMLDivElement>('#history-loading');
|
const loadingEl = container.querySelector<HTMLDivElement>('#history-loading');
|
||||||
|
|
||||||
let items: HistoryItem[] = [];
|
let items: HistoryItem[] = [];
|
||||||
let activeId: number | null = null;
|
const activeId: number | null = null;
|
||||||
|
|
||||||
function showLoading() {
|
function showLoading() {
|
||||||
loadingEl?.classList.remove('hidden');
|
loadingEl?.classList.remove('hidden');
|
||||||
@@ -283,7 +283,7 @@ const { tier, userId } = Astro.props;
|
|||||||
if (tier === 'pro' && userId) {
|
if (tier === 'pro' && userId) {
|
||||||
showLoading();
|
showLoading();
|
||||||
try {
|
try {
|
||||||
items = await fetchProHistory(userId);
|
items = await fetchProHistory();
|
||||||
} catch {
|
} catch {
|
||||||
items = [];
|
items = [];
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
@@ -184,7 +184,6 @@ const { tier = 'free', userId } = Astro.props;
|
|||||||
|
|
||||||
let activeIndex = 0;
|
let activeIndex = 0;
|
||||||
let currentSessionId: number | null = null;
|
let currentSessionId: number | null = null;
|
||||||
let currentSessionName = '';
|
|
||||||
|
|
||||||
interface CombatantData {
|
interface CombatantData {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -559,7 +558,6 @@ const { tier = 'free', userId } = Astro.props;
|
|||||||
saveCombatants(session.participants);
|
saveCombatants(session.participants);
|
||||||
activeIndex = 0;
|
activeIndex = 0;
|
||||||
currentSessionId = session.id;
|
currentSessionId = session.id;
|
||||||
currentSessionName = session.name;
|
|
||||||
if (sessionNameInput) sessionNameInput.value = session.name;
|
if (sessionNameInput) sessionNameInput.value = session.name;
|
||||||
updateVisibility();
|
updateVisibility();
|
||||||
renderList();
|
renderList();
|
||||||
@@ -578,7 +576,6 @@ const { tier = 'free', userId } = Astro.props;
|
|||||||
const result = await apiSaveSession(name, participants, currentSessionId ?? undefined);
|
const result = await apiSaveSession(name, participants, currentSessionId ?? undefined);
|
||||||
if (result) {
|
if (result) {
|
||||||
currentSessionId = result.id;
|
currentSessionId = result.id;
|
||||||
currentSessionName = result.name;
|
|
||||||
if (sessionNameInput) sessionNameInput.value = result.name;
|
if (sessionNameInput) sessionNameInput.value = result.name;
|
||||||
showSessionStatus('Сессия сохранена');
|
showSessionStatus('Сессия сохранена');
|
||||||
renderSessionList();
|
renderSessionList();
|
||||||
@@ -589,7 +586,6 @@ const { tier = 'free', userId } = Astro.props;
|
|||||||
|
|
||||||
async function handleNewSession() {
|
async function handleNewSession() {
|
||||||
currentSessionId = null;
|
currentSessionId = null;
|
||||||
currentSessionName = '';
|
|
||||||
if (sessionNameInput) sessionNameInput.value = '';
|
if (sessionNameInput) sessionNameInput.value = '';
|
||||||
clearAll();
|
clearAll();
|
||||||
showSessionStatus('Новая сессия');
|
showSessionStatus('Новая сессия');
|
||||||
@@ -602,7 +598,6 @@ const { tier = 'free', userId } = Astro.props;
|
|||||||
if (ok) {
|
if (ok) {
|
||||||
if (currentSessionId === id) {
|
if (currentSessionId === id) {
|
||||||
currentSessionId = null;
|
currentSessionId = null;
|
||||||
currentSessionName = '';
|
|
||||||
if (sessionNameInput) sessionNameInput.value = '';
|
if (sessionNameInput) sessionNameInput.value = '';
|
||||||
}
|
}
|
||||||
renderSessionList();
|
renderSessionList();
|
||||||
|
|||||||
@@ -49,7 +49,6 @@ const { tier = 'free', userId } = Astro.props;
|
|||||||
let debounceTimer: ReturnType<typeof setTimeout> | null = null;
|
let debounceTimer: ReturnType<typeof setTimeout> | null = null;
|
||||||
let hideIndicatorTimer: ReturnType<typeof setTimeout> | null = null;
|
let hideIndicatorTimer: ReturnType<typeof setTimeout> | null = null;
|
||||||
let dbNoteId: number | null = null;
|
let dbNoteId: number | null = null;
|
||||||
let isLoading = false;
|
|
||||||
|
|
||||||
function formatTime(date: Date): string {
|
function formatTime(date: Date): string {
|
||||||
return date.toLocaleTimeString('ru-RU', { hour: '2-digit', minute: '2-digit' });
|
return date.toLocaleTimeString('ru-RU', { hour: '2-digit', minute: '2-digit' });
|
||||||
@@ -123,23 +122,19 @@ const { tier = 'free', userId } = Astro.props;
|
|||||||
|
|
||||||
async function loadSavedNotes() {
|
async function loadSavedNotes() {
|
||||||
if (isPro) {
|
if (isPro) {
|
||||||
isLoading = true;
|
|
||||||
const note = await loadDbNotes();
|
const note = await loadDbNotes();
|
||||||
if (note) {
|
if (note) {
|
||||||
dbNoteId = note.id;
|
dbNoteId = note.id;
|
||||||
textarea.value = note.content ?? '';
|
textarea.value = note.content ?? '';
|
||||||
} else {
|
} else {
|
||||||
// Try to create a new note if none exists
|
|
||||||
const newId = await createDbNote('');
|
const newId = await createDbNote('');
|
||||||
if (newId) {
|
if (newId) {
|
||||||
dbNoteId = newId;
|
dbNoteId = newId;
|
||||||
textarea.value = '';
|
textarea.value = '';
|
||||||
} else {
|
} else {
|
||||||
// Fallback to localStorage if DB is unavailable
|
|
||||||
textarea.value = loadNotes();
|
textarea.value = loadNotes();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
isLoading = false;
|
|
||||||
} else {
|
} else {
|
||||||
const saved = loadNotes();
|
const saved = loadNotes();
|
||||||
textarea.value = saved;
|
textarea.value = saved;
|
||||||
|
|||||||
@@ -171,11 +171,7 @@ import DmButton from "./DmButton.astro";
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { Open5eUIManager } from "@/lib/client/open5e-ui";
|
import { Open5eUIManager } from "@/lib/client/open5e-ui";
|
||||||
import {
|
import { fetchTranslation } from "@/lib/client/translation";
|
||||||
getCachedTranslation,
|
|
||||||
setCachedTranslation,
|
|
||||||
fetchTranslation,
|
|
||||||
} from "@/lib/client/translation";
|
|
||||||
|
|
||||||
const CR_OPTIONS = [
|
const CR_OPTIONS = [
|
||||||
{ value: "", label: "Все ОП" },
|
{ value: "", label: "Все ОП" },
|
||||||
@@ -544,12 +540,10 @@ import DmButton from "./DmButton.astro";
|
|||||||
btn.textContent = "Переведено";
|
btn.textContent = "Переведено";
|
||||||
btn.disabled = true;
|
btn.disabled = true;
|
||||||
|
|
||||||
// Re-render detail with translation
|
|
||||||
const state = manager.state;
|
const state = manager.state;
|
||||||
if (state.selectedItem && state.selectedItem.key === slug) {
|
if (state.selectedItem && state.selectedItem.key === slug) {
|
||||||
const cached = getCachedTranslation(slug, type);
|
const isTranslated = translated !== null;
|
||||||
const isTranslated = cached !== null;
|
const item = mergeWithTranslation(state.selectedItem, translated);
|
||||||
const item = mergeWithTranslation(state.selectedItem, cached);
|
|
||||||
detailContent.innerHTML = state.tab === "monsters"
|
detailContent.innerHTML = state.tab === "monsters"
|
||||||
? renderMonsterDetail(item as Monster, isTranslated)
|
? renderMonsterDetail(item as Monster, isTranslated)
|
||||||
: renderSpellDetail(item as Spell, isTranslated);
|
: renderSpellDetail(item as Spell, isTranslated);
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import { z } from "zod";
|
|
||||||
import {
|
import {
|
||||||
npcParamsSchema,
|
npcParamsSchema,
|
||||||
npcResultSchema,
|
npcResultSchema,
|
||||||
|
|||||||
@@ -1,6 +1,4 @@
|
|||||||
import { z } from "zod";
|
|
||||||
import { npcResultSchema, type NPCResult, type NPCParams, type Open5eMonster } from "@/lib/ai/types";
|
import { npcResultSchema, type NPCResult, type NPCParams, type Open5eMonster } from "@/lib/ai/types";
|
||||||
import type { Monster } from "@/lib/open5e/client";
|
|
||||||
|
|
||||||
const OPENROUTER_API_URL = "https://openrouter.ai/api/v1/chat/completions";
|
const OPENROUTER_API_URL = "https://openrouter.ai/api/v1/chat/completions";
|
||||||
const DEFAULT_MODEL = "llama-3.3-70b:free";
|
const DEFAULT_MODEL = "llama-3.3-70b:free";
|
||||||
@@ -121,7 +119,7 @@ export async function translateOpen5eContent(
|
|||||||
return parsed as Record<string, unknown>;
|
return parsed as Record<string, unknown>;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (err instanceof Error && err.name === "AbortError") {
|
if (err instanceof Error && err.name === "AbortError") {
|
||||||
throw new Error("OpenRouter request timed out after 10s");
|
throw new Error("OpenRouter request timed out after 10s", { cause: err });
|
||||||
}
|
}
|
||||||
throw err;
|
throw err;
|
||||||
} finally {
|
} finally {
|
||||||
@@ -197,7 +195,7 @@ export async function generateNPC(
|
|||||||
return validated.data;
|
return validated.data;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (err instanceof Error && err.name === "AbortError") {
|
if (err instanceof Error && err.name === "AbortError") {
|
||||||
throw new Error("OpenRouter request timed out after 10s");
|
throw new Error("OpenRouter request timed out after 10s", { cause: err });
|
||||||
}
|
}
|
||||||
throw err;
|
throw err;
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
@@ -142,6 +142,7 @@ export const POST: APIRoute = async ({ request, locals }) => {
|
|||||||
try {
|
try {
|
||||||
await incrementGenerationCounter(user.id, modelName);
|
await incrementGenerationCounter(user.id, modelName);
|
||||||
} catch {
|
} catch {
|
||||||
|
// best-effort: counter increment failures should not block the response
|
||||||
}
|
}
|
||||||
|
|
||||||
const response: { npc: NPCResult & { id: number }; reference?: Open5eMonster } = {
|
const response: { npc: NPCResult & { id: number }; reference?: Open5eMonster } = {
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import type { APIRoute } from "astro";
|
|||||||
import { jsonResponse, handleCorsPreflight } from "@/lib/cors";
|
import { jsonResponse, handleCorsPreflight } from "@/lib/cors";
|
||||||
import { db } from "@/db/client";
|
import { db } from "@/db/client";
|
||||||
import { npcs } from "@/db/schema";
|
import { npcs } from "@/db/schema";
|
||||||
import { eq, desc, sql, count } from "drizzle-orm";
|
import { eq, desc, count } from "drizzle-orm";
|
||||||
|
|
||||||
export const prerender = false;
|
export const prerender = false;
|
||||||
|
|
||||||
|
|||||||
@@ -37,11 +37,15 @@ function mockRequest(
|
|||||||
} as unknown as Request;
|
} as unknown as Request;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// vi.mock factories are hoisted, so refs they read must use var (not let/const)
|
||||||
|
// to be visible at hoist time without ReferenceError.
|
||||||
|
/* eslint-disable no-var */
|
||||||
var mockOpenRouterNPC = vi.fn();
|
var mockOpenRouterNPC = vi.fn();
|
||||||
var mockKimiNPC = vi.fn();
|
var mockKimiNPC = vi.fn();
|
||||||
var mockSearchMonsters = vi.fn();
|
var mockSearchMonsters = vi.fn();
|
||||||
var mockCheckRateLimit = vi.fn();
|
var mockCheckRateLimit = vi.fn();
|
||||||
var mockIncrementCounter = vi.fn();
|
var mockIncrementCounter = vi.fn();
|
||||||
|
/* eslint-enable no-var */
|
||||||
|
|
||||||
vi.mock("@/lib/ai/openrouter", () => ({
|
vi.mock("@/lib/ai/openrouter", () => ({
|
||||||
generateNPC: (...args: unknown[]) => mockOpenRouterNPC(...args),
|
generateNPC: (...args: unknown[]) => mockOpenRouterNPC(...args),
|
||||||
|
|||||||
@@ -96,7 +96,7 @@ function createMockDb() {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
let mockDb = createMockDb();
|
const mockDb = createMockDb();
|
||||||
|
|
||||||
vi.mock("drizzle-orm", async (importOriginal) => {
|
vi.mock("drizzle-orm", async (importOriginal) => {
|
||||||
const actual = await importOriginal<typeof import("drizzle-orm")>();
|
const actual = await importOriginal<typeof import("drizzle-orm")>();
|
||||||
|
|||||||
@@ -12,18 +12,6 @@ const mockUser = {
|
|||||||
createdAt: new Date("2024-01-01"),
|
createdAt: new Date("2024-01-01"),
|
||||||
};
|
};
|
||||||
|
|
||||||
const mockUser2 = {
|
|
||||||
id: 2,
|
|
||||||
vkId: null,
|
|
||||||
yandexId: null,
|
|
||||||
email: null,
|
|
||||||
name: "Other User",
|
|
||||||
avatar: null,
|
|
||||||
tier: "free" as const,
|
|
||||||
boostyVerifiedAt: null,
|
|
||||||
createdAt: new Date("2024-01-01"),
|
|
||||||
};
|
|
||||||
|
|
||||||
function mockRequest(
|
function mockRequest(
|
||||||
url: string,
|
url: string,
|
||||||
origin: string,
|
origin: string,
|
||||||
@@ -132,7 +120,7 @@ function createMockDb() {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
let mockDb = createMockDb();
|
const mockDb = createMockDb();
|
||||||
|
|
||||||
vi.mock("drizzle-orm", async (importOriginal) => {
|
vi.mock("drizzle-orm", async (importOriginal) => {
|
||||||
const actual = await importOriginal<typeof import("drizzle-orm")>();
|
const actual = await importOriginal<typeof import("drizzle-orm")>();
|
||||||
|
|||||||
+1
-13
@@ -12,18 +12,6 @@ const mockUser = {
|
|||||||
createdAt: new Date("2024-01-01"),
|
createdAt: new Date("2024-01-01"),
|
||||||
};
|
};
|
||||||
|
|
||||||
const mockUser2 = {
|
|
||||||
id: 2,
|
|
||||||
vkId: null,
|
|
||||||
yandexId: null,
|
|
||||||
email: null,
|
|
||||||
name: "Other User",
|
|
||||||
avatar: null,
|
|
||||||
tier: "free" as const,
|
|
||||||
boostyVerifiedAt: null,
|
|
||||||
createdAt: new Date("2024-01-01"),
|
|
||||||
};
|
|
||||||
|
|
||||||
function mockRequest(
|
function mockRequest(
|
||||||
url: string,
|
url: string,
|
||||||
origin: string,
|
origin: string,
|
||||||
@@ -132,7 +120,7 @@ function createMockDb() {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
let mockDb = createMockDb();
|
const mockDb = createMockDb();
|
||||||
|
|
||||||
vi.mock("drizzle-orm", async (importOriginal) => {
|
vi.mock("drizzle-orm", async (importOriginal) => {
|
||||||
const actual = await importOriginal<typeof import("drizzle-orm")>();
|
const actual = await importOriginal<typeof import("drizzle-orm")>();
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import type { Monster, Spell } from "../src/lib/open5e/client";
|
|||||||
// If a listed key is missing from the interface, TypeScript will error at compile time.
|
// If a listed key is missing from the interface, TypeScript will error at compile time.
|
||||||
type AssertKeys<T, K extends keyof T> = K;
|
type AssertKeys<T, K extends keyof T> = K;
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
type _MonsterHasAllFields = AssertKeys<
|
type _MonsterHasAllFields = AssertKeys<
|
||||||
Monster,
|
Monster,
|
||||||
| "name"
|
| "name"
|
||||||
@@ -34,6 +35,7 @@ type _MonsterHasAllFields = AssertKeys<
|
|||||||
| "condition_immunities"
|
| "condition_immunities"
|
||||||
>;
|
>;
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
type _SpellHasAllFields = AssertKeys<
|
type _SpellHasAllFields = AssertKeys<
|
||||||
Spell,
|
Spell,
|
||||||
| "name"
|
| "name"
|
||||||
|
|||||||
@@ -77,7 +77,7 @@ function createMockDb() {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
let mockDb = createMockDb();
|
const mockDb = createMockDb();
|
||||||
|
|
||||||
vi.mock("drizzle-orm", async (importOriginal) => {
|
vi.mock("drizzle-orm", async (importOriginal) => {
|
||||||
const actual = await importOriginal<typeof import("drizzle-orm")>();
|
const actual = await importOriginal<typeof import("drizzle-orm")>();
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { describe, it, expect, vi, beforeEach } from "vitest";
|
|||||||
const mockPostMessage = vi.fn();
|
const mockPostMessage = vi.fn();
|
||||||
const mockAddEventListener = vi.fn();
|
const mockAddEventListener = vi.fn();
|
||||||
|
|
||||||
function MockBroadcastChannel(this: unknown, _name: string) {
|
function MockBroadcastChannel(this: unknown) {
|
||||||
return {
|
return {
|
||||||
postMessage: mockPostMessage,
|
postMessage: mockPostMessage,
|
||||||
addEventListener: mockAddEventListener,
|
addEventListener: mockAddEventListener,
|
||||||
@@ -75,7 +75,6 @@ describe("translation service", () => {
|
|||||||
global.fetch = mockFetch as unknown as typeof fetch;
|
global.fetch = mockFetch as unknown as typeof fetch;
|
||||||
|
|
||||||
const {
|
const {
|
||||||
getCachedTranslation,
|
|
||||||
setCachedTranslation,
|
setCachedTranslation,
|
||||||
fetchTranslation,
|
fetchTranslation,
|
||||||
clearTranslationCache,
|
clearTranslationCache,
|
||||||
|
|||||||
Reference in New Issue
Block a user