Files
Chris Arderne c7861be520 chore: activate no-unused-vars and import linters (#4096)
Once this is merged, oxlint is at a pretty sensible baseline.

**Enable `no-unused-vars`, `typescript/consistent-type-imports`, and
`import/no-duplicates` lint rules**

Turns on three previously-disabled oxlint rules across the monorepo and
fixes all violations:

- **`no-unused-vars`** – enabled as an error with standard ignore
patterns: unused function arguments are ignored by default (`args:
"none"`), variables/caught errors/destructured array elements prefixed
with `_` are allowed, and rest siblings are permitted.
- **`typescript/consistent-type-imports`** – enforced as an error; all
type-only imports now use the `import type` syntax.
- **`import/no-duplicates`** – enforced as an error; duplicate import
statements from the same module have been merged.

The remaining commits clean up the violations found across the codebase:
removing unused variables/imports/type aliases, adding `_` prefixes to
intentionally unused bindings, fixing duplicate imports, and converting
value imports to `import type` where appropriate.
2026-07-02 11:37:05 +01:00

486 lines
14 KiB
TypeScript

import { execa } from "execa";
import * as nodePath from "node:path";
import * as fs from "node:fs";
import { logger } from "../src/utilities/logger.js";
import { findUpMultiple, findUp } from "find-up";
import type { TaskRunExecutionResult, WorkerManifest } from "@trigger.dev/core/v3/schemas";
import { TaskRunProcess } from "../src/executions/taskRunProcess.js";
import { createTestHttpServer } from "@epic-web/test-server/http";
import type { TestCase, TestCaseRun } from "./fixtures.js";
import { access } from "node:fs/promises";
import type { MachinePreset } from "@trigger.dev/core/v3";
export type PackageManager = "npm" | "pnpm" | "yarn";
export const LOCKFILES = {
npm: "package-lock.json",
npmShrinkwrap: "npm-shrinkwrap.json",
pnpm: "pnpm-lock.yaml",
yarn: "yarn.lock",
bun: "bun.lockb",
};
export async function installFixtureDeps(options: {
fixtureDir: string;
packageManager: PackageManager;
workspaceDir: string;
}) {
const { packageManager, workspaceDir } = options;
if (["pnpm", "yarn"].includes(packageManager)) {
const version = await detectPackageManagerVersion(options);
debug(`Detected ${packageManager}@${version} from package.json 'engines' field`);
const { stdout, stderr } = await execa("corepack", ["use", `${packageManager}@${version}`], {
cwd: workspaceDir,
});
debug(stdout);
if (stderr) console.error(stderr);
} else {
const { stdout, stderr } = await execa(packageManager, installArgs(packageManager), {
cwd: workspaceDir,
env: {
...process.env,
NODE_PATH: nodePath.resolve(nodePath.join(workspaceDir, "node_modules")),
},
});
debug(stdout);
if (stderr) console.error(stderr);
}
}
export async function parsePackageManager(
packageManager: PackageManager | undefined,
fixtureDir: string
): Promise<PackageManager> {
let $packageManager: PackageManager;
if (packageManager) {
$packageManager = packageManager;
} else {
$packageManager = await detectPackageManagerFromArtifacts(fixtureDir);
}
return $packageManager;
}
export async function detectPackageManagerFromArtifacts(path: string): Promise<PackageManager> {
const foundPath = await findUp(Object.values(LOCKFILES), { cwd: path });
if (!foundPath) {
throw new Error("Could not detect package manager from artifacts");
}
logger.debug("Found path from package manager artifacts", { foundPath });
switch (nodePath.basename(foundPath)) {
case LOCKFILES.yarn:
logger.debug("Found yarn artifact", { foundPath });
return "yarn";
case LOCKFILES.pnpm:
logger.debug("Found pnpm artifact", { foundPath });
return "pnpm";
case LOCKFILES.npm:
case LOCKFILES.npmShrinkwrap:
logger.debug("Found npm artifact", { foundPath });
return "npm";
case LOCKFILES.bun:
logger.debug("Found bun artifact", { foundPath });
return "npm";
default:
throw new Error(`Unhandled package manager detection path: ${foundPath}`);
}
}
function debug(message: string) {
if (logger.loggerLevel === "debug") {
console.log(message);
}
}
function installArgs(packageManager: string) {
switch (packageManager) {
case "bun":
return ["install", "--frozen-lockfile"];
case "pnpm":
case "yarn":
throw new Error("pnpm and yarn must install using `corepack use`");
case "npm":
return ["ci", "--no-audit"];
default:
throw new Error(`Unknown package manager '${packageManager}'`);
}
}
async function detectPackageManagerVersion(options: {
fixtureDir: string;
packageManager: PackageManager;
workspaceDir: string;
}): Promise<string> {
const { fixtureDir, packageManager, workspaceDir } = options;
const pkgPaths = await findUpMultiple("package.json", { cwd: workspaceDir, stopAt: fixtureDir });
for (let pkgPath of pkgPaths) {
const buffer = fs.readFileSync(pkgPath, "utf8");
const pkgJSON = JSON.parse(buffer.toString());
if (!pkgJSON.engines) continue;
const version = pkgJSON.engines[packageManager];
if (version) return version;
}
throw new Error(`No version found for package manager ${packageManager}`);
}
export interface TypeScriptError {
file: string;
line: number;
column: number;
message: string;
}
export interface TscResult {
success: boolean;
errors: TypeScriptError[];
stdout: string;
stderr: string;
}
export async function runTsc(
cwd: string,
tsconfigName: string = "tsconfig.json",
binBasePath: string = cwd
): Promise<TscResult> {
const tsconfigPath = nodePath.join(cwd, tsconfigName);
const tscPath = nodePath.join(binBasePath, "node_modules", ".bin", "tsc");
// Ensure the tsconfig file exists
try {
await access(tsconfigPath);
} catch (_error) {
throw new Error(`TSConfig file not found: ${tsconfigPath}`);
}
try {
logger.debug(`Running TypeScript compiler: ${tscPath} --project ${tsconfigPath} --noEmit`, {
cwd,
});
const result = await execa(tscPath, ["--project", tsconfigPath, "--noEmit"], {
cwd,
reject: false,
});
const success = result.exitCode === 0;
const errors = success ? [] : parseTypeScriptErrors(result.stderr);
logger.debug(result.stdout);
logger.debug(result.stderr);
return {
success,
errors,
stdout: result.stdout,
stderr: result.stderr,
};
} catch (error) {
throw new Error(`Failed to run TypeScript compiler: ${error}`);
}
}
function parseTypeScriptErrors(stderr: string): TypeScriptError[] {
const errorRegex = /(.+)\((\d+),(\d+)\):\s+error\s+TS\d+:\s+(.+)/g;
const errors: TypeScriptError[] = [];
let match;
while ((match = errorRegex.exec(stderr)) !== null) {
errors.push({
file: match[1]!,
line: parseInt(match[2]!, 10),
column: parseInt(match[3]!, 10),
message: match[4]!,
});
}
return errors;
}
export type ExecuteTaskCaseRunOptions = {
testCase: TestCase;
run: TestCaseRun;
destination: string;
workerManifest: WorkerManifest;
contentHash: string;
};
export type ExecuteTaskRunUsageReport = {
durationMs: number;
};
export type ExecuteTaskRunResult = {
result: TaskRunExecutionResult;
usageReports: Array<ExecuteTaskRunUsageReport>;
totalDurationMs: number;
spans: Array<ExecuteTaskTraceEvent>;
};
export type ExecuteTaskTraceEvent = {
name: string;
traceId: string;
spanId: string;
parentSpanId?: string;
durationMs: number;
attributes?: { [key: string]: string | number | boolean | undefined };
};
export async function executeTestCaseRun({
run,
testCase,
destination,
workerManifest,
contentHash,
}: ExecuteTaskCaseRunOptions): Promise<ExecuteTaskRunResult> {
const usageReports: Array<ExecuteTaskRunUsageReport> = [];
const spans: Array<ExecuteTaskTraceEvent> = [];
// Create a disposable "server" instance.
const server = await createTestHttpServer({
defineRoutes(router) {
router.post("/usage", async ({ req }) => {
const jsonBody = await req.json();
usageReports.push({
durationMs: jsonBody.durationMs,
});
return Response.json({});
});
router.post("/v1/traces", async ({ req }) => {
const jsonBody = await req.json();
spans.push(...parseTraceBodyIntoEvents(jsonBody));
// TODO: Implement trace endpoint
return Response.json({});
});
router.post("/v1/logs", () => {
// TODO: Implement logs endpoint
return Response.json({});
});
router.post("/v1/chat/completions", async ({ req }) => {
return Response.json({
id: "chatcmpl-7XYZ123ABC456DEF789GHI",
object: "chat.completion",
created: 1631619199,
model: "gpt-3.5-turbo-0613",
choices: [
{
index: 0,
message: {
role: "assistant",
content:
"The capital of France is Paris. Paris is not only the political capital but also the cultural and economic center of France. It's known for its iconic landmarks such as the Eiffel Tower, the Louvre Museum, and Notre-Dame Cathedral.",
},
finish_reason: "stop",
},
],
usage: {
prompt_tokens: 29,
completion_tokens: 48,
total_tokens: 77,
},
});
});
},
});
const machine = {
name: "small-1x",
cpu: 1,
memory: 256,
centsPerMs: 0.0000001,
} satisfies MachinePreset;
try {
const taskRunProcess = new TaskRunProcess({
workerManifest: workerManifest!,
cwd: destination,
env: {
USAGE_EVENT_URL: server.http.url("/usage").href,
OTEL_EXPORTER_OTLP_ENDPOINT: server.http.url().origin,
TRIGGER_JWT: "test-jwt",
TRIGGER_SECRET_KEY: "test-secret",
TRIGGER_API_URL: server.http.url().origin,
USAGE_HEARTBEAT_INTERVAL_MS: "500",
OPENAI_API_KEY: "api-key",
OPENAI_BASE_URL: server.http.url().origin + "/v1",
},
serverWorker: {
id: "test",
version: "1.0.0",
contentHash,
},
machineResources: machine,
}).initialize();
const result = await taskRunProcess.execute({
payload: {
traceContext: {
traceparent: "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01",
},
environment: {},
execution: {
task: run.task,
attempt: {
id: "attempt_1234",
status: "RUNNING",
number: 1,
startedAt: new Date(),
backgroundWorkerId: "worker_1234",
backgroundWorkerTaskId: "task_1234",
},
run: {
id: "run_1234",
startedAt: new Date(),
payload: run.payload,
payloadType: run.payloadType ?? "application/json",
tags: [],
context: {},
isTest: false,
createdAt: new Date(),
durationMs: 0,
costInCents: 0,
baseCostInCents: 0,
version: "1.0.0",
},
queue: {
id: "queue_1234",
name: "test",
},
environment: {
type: "DEVELOPMENT",
id: "env_1234",
slug: "dev",
},
organization: {
id: "org_1234",
slug: "test",
name: "test",
},
project: {
id: "project_1234",
slug: "test",
ref: "main",
name: "test",
},
machine,
},
},
messageId: "run_1234",
});
await taskRunProcess.cleanup(true);
return {
result,
usageReports,
totalDurationMs: usageReports.reduce((acc, report) => acc + report.durationMs, 0),
spans,
};
} finally {
await server.close();
}
}
function parseTraceBodyIntoEvents(body: any): ExecuteTaskTraceEvent[] {
return body.resourceSpans.flatMap(parseResourceSpanIntoEvents);
}
function parseResourceSpanIntoEvents(resourceSpan: any): ExecuteTaskTraceEvent[] {
return resourceSpan.scopeSpans.flatMap((scopeSpan: any) =>
parseScopeSpanIntoEvents(scopeSpan, resourceSpan.resource)
);
}
function parseScopeSpanIntoEvents(scopeSpan: any, resource: any): ExecuteTaskTraceEvent[] {
return scopeSpan.spans.flatMap((span: any) => parseSpanInEvent(span, resource));
}
function parseSpanInEvent(span: any, resource: any): ExecuteTaskTraceEvent {
return {
name: span.name,
traceId: span.traceId,
spanId: span.spanId,
parentSpanId: span.parentSpanId,
durationMs: calculateSpanDurationMs(span),
attributes: {
...parseAttributes(resource.attributes),
...parseAttributes(span.attributes),
},
};
}
function calculateSpanDurationMs(span: any): number {
return Number(BigInt(span.endTimeUnixNano) - BigInt(span.startTimeUnixNano) / BigInt(1e6));
}
function parseAttributes(attributes: any): ExecuteTaskTraceEvent["attributes"] {
if (!attributes) return {};
return attributes.reduce((acc: any, attribute: any) => {
acc[attribute.key] = isStringValue(attribute.value)
? attribute.value.stringValue
: isIntValue(attribute.value)
? Number(attribute.value.intValue)
: isDoubleValue(attribute.value)
? attribute.value.doubleValue
: isBoolValue(attribute.value)
? attribute.value.boolValue
: isBytesValue(attribute.value)
? binaryToHex(attribute.value.bytesValue)
: undefined;
return acc;
}, {});
}
function isBoolValue(value: any | undefined): value is { boolValue: boolean } {
if (!value) return false;
return typeof value.boolValue === "boolean";
}
function isStringValue(value: any | undefined): value is { stringValue: string } {
if (!value) return false;
return typeof value.stringValue === "string";
}
function isIntValue(value: any | undefined): value is { intValue: bigint } {
if (!value) return false;
return typeof value.intValue === "number";
}
function isDoubleValue(value: any | undefined): value is { doubleValue: number } {
if (!value) return false;
return typeof value.doubleValue === "number";
}
function isBytesValue(value: any | undefined): value is { bytesValue: Buffer } {
if (!value) return false;
return Buffer.isBuffer(value.bytesValue);
}
function binaryToHex(buffer: Buffer | string): string;
function binaryToHex(buffer: Buffer | string | undefined): string | undefined;
function binaryToHex(buffer: Buffer | string | undefined): string | undefined {
if (!buffer) return undefined;
if (typeof buffer === "string") return buffer;
return Buffer.from(Array.from(buffer)).toString("hex");
}