diff --git a/doc/DEVELOPING.md b/doc/DEVELOPING.md index 1023f2f1414..494ef747545 100644 --- a/doc/DEVELOPING.md +++ b/doc/DEVELOPING.md @@ -1443,6 +1443,11 @@ The server typecheck builds only the Runner's TypeScript output. Use `pnpm --filter @paperclipai/paperclip-runner build` when a local check needs the Runner binary. +If a compiler child terminates on a signal, the wrapper reports the signal, +layer, command, and working directory on stderr, then stops with a nonzero exit. +Compiler stdout and stderr remain visible. A signal such as `SIGKILL` alone does +not establish that the process ran out of memory. + `src/__tests__/tsconfig.json` holds the options for the test-tree layers, so an editor and the typecheck report the same errors. Server tests reach Daytona plugin source, but the root install does not install that plugin's dependencies. So this diff --git a/server/scripts/typecheck-layers.mjs b/server/scripts/typecheck-layers.mjs index 7791e3671f6..ea775d40e67 100644 --- a/server/scripts/typecheck-layers.mjs +++ b/server/scripts/typecheck-layers.mjs @@ -288,7 +288,12 @@ export function typecheckLayers({ const config = path.join(OUTPUT_DIRECTORY, `tsconfig.${layer.name}.json`); const { status, signal } = run(["-p", config], { cwd: serverRoot }); // A killed process leaves no declarations, so later layers would only add noise. - if (signal) return status || 1; + if (signal) { + console.error( + `typecheck:layers: ${layer.name}: compiler terminated by ${signal} (tsc -p ${config}, cwd: ${serverRoot})`, + ); + return status || 1; + } // Later layers still run after type errors. Each file is checked in one // layer, so this reports everything a single tsc run would. if (status !== 0) exitCode = status; diff --git a/server/scripts/typecheck-layers.test.mjs b/server/scripts/typecheck-layers.test.mjs index 97ee651b6ce..8239fea0c4d 100644 --- a/server/scripts/typecheck-layers.test.mjs +++ b/server/scripts/typecheck-layers.test.mjs @@ -319,6 +319,38 @@ describe("typecheckLayers", () => { expect(status).toBe(1); expect(configs).toHaveLength(1); }); + + it.each([ + ["SIGTERM", "below"], + ["SIGKILL", "cycle"], + ])("reports %s and the affected %s layer from a real compiler child", (signal, layer) => { + const root = serverFixture(); + const bin = path.join(root, "node_modules", "typescript", "bin"); + mkdirSync(bin, { recursive: true }); + writeFileSync(path.join(bin, "tsc"), ` + const fs = require("node:fs"); + fs.writeSync(1, "compiler stdout\\n"); + fs.writeSync(2, "compiler stderr\\n"); + if (process.argv[3].endsWith("tsconfig.${layer}.json")) { + process.kill(process.pid, ${JSON.stringify(signal)}); + } + `); + const wrapper = new URL("./typecheck-layers.mjs", import.meta.url).href; + + const result = spawnSync(process.execPath, ["--input-type=module", "-e", ` + import { typecheckLayers } from ${JSON.stringify(wrapper)}; + process.exitCode = typecheckLayers({ serverRoot: ${JSON.stringify(root)} }); + `], { encoding: "utf8", timeout: 10_000 }); + + expect(result.error).toBeUndefined(); + expect(result.signal).toBeNull(); + expect(result.status).toBe(1); + expect(result.stdout).toContain("compiler stdout"); + expect(result.stderr).toContain("compiler stderr"); + expect(result.stderr).toContain(`typecheck:layers: ${layer}: compiler terminated by ${signal}`); + expect(result.stderr).toContain(`tsconfig.${layer}.json`); + expect(result.stdout).not.toContain("typecheck:layers: above"); + }); }); describe("typecheckLayers with the real compiler", () => {