You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在GraphQL控制器中使用fs.watch监听文件并正确返回测试结果

问题原因
  • fs.watch()的回调是异步执行的,回调内部的return仅作用于回调函数本身,不会作为外层postSolution方法的返回值
  • 原postSolution方法注册完监听逻辑后没有等待回调执行就直接结束,异步方法无显式返回时默认返回null,所以你调用mutation会拿到null
  • 另外原代码中tests.forEach(async () => {})的写法不会等待所有测试文件写入完成,后续逻辑执行时可能出现文件缺失的问题
修复方案

将监听逻辑封装为Promise,让postSolution等待测试结果返回后再响应,同时修复测试文件写入的异步等待问题,修改后的完整代码如下:

import { Context } from "../typescript/types";
import TestModel from "../models/TestModel";

const fs = require("fs-extra");

interface InputSolution {
  userId: string;
  exerciseId: string;
  testIDs: string[];
  solution: string;
}

export default class SandboxController {
  async postSolution(
    parent: any,
    args: { input: InputSolution },
    context: Context
  ) {
    // 1. 清空src、spec目录下的所有文件以及输出json文件
    try {
      await fs.emptyDir("./userCode/src");
      await fs.emptyDir("./userCode/spec");
      console.log("目录清空成功");
    } catch (err) {
      console.error(err);
      throw new Error("代码目录清空失败");
    }

    // 2. 写入待测试函数和测试用例文件
    const tests = await TestModel.find()
      .where("_id")
      .in(args.input.testIDs)
      .exec();

    try {
      await fs.writeFile("./userCode/src/solution.js", args.input.solution);
      // 用Promise.all等待所有测试文件写入完成
      await Promise.all(tests.map(async (test, index) => {
        await fs.writeFile(
          `./userCode/spec/test${index}.spec.js`,
          test.content
        );
      }));
      console.log("测试文件写入成功");
    } catch (err) {
      console.error(err);
      throw new Error("测试文件写入失败");
    }

    // 3. 写入flag文件标记更新完成
    try {
      await fs.writeFile(
        "./userCode/flag.txt",
        args.input.exerciseId + " - " + args.input.userId
      );
      console.log("flag文件写入成功");
    } catch (err) {
      console.error(err);
      throw new Error("flag文件写入失败");
    }

    // 4. 监听flag文件变化,封装为Promise等待测试结果
    return new Promise((resolve, reject) => {
      // 加超时逻辑,避免测试异常导致请求一直挂起
      const timeout = setTimeout(() => {
        watcher.close();
        reject(new Error("测试执行超时"));
      }, 30000); // 30秒超时可自行调整

      const watcher = fs.watch("./userCode/flag.txt", async (eventType: string, filename: string) => {
        // 触发后立即关闭监听器,避免重复回调
        watcher.close();
        clearTimeout(timeout);

        console.log(`事件类型: ${eventType}`);
        try {
          const json = await fs.readFile(
            "./userCode/testLogs/output.json",
            "utf8"
          );
          const output: any = json ? JSON.parse(json) : null;

          if (output && (output.numTotalTestSuites > 0 || output.numTotalTests > 0)) {
            console.log("测试套件总数", output.numTotalTestSuites);
            if (output.numFailedTestSuites === 0 && output.numFailedTests === 0) {
              console.log("所有测试通过");
              resolve([{ testId: "12", passed: true }]);
            } else {
              console.log("测试失败,失败套件数", output.numFailedTestSuites);
              resolve([{ testId: "12", passed: false }]);
            }
          } else {
            resolve([{ testId: "12", passed: false }]);
          }
        } catch (err) {
          console.error("读取测试结果失败", err);
          resolve([{ testId: "12", passed: false }]);
        }
      });
    });
  }
}
补充注意事项
  • 新增的超时逻辑可以避免测试运行出错时请求长时间无响应,超时时间可根据你的测试运行时长自行调整
  • 原代码中异常仅做了打印处理,修改后异常会直接抛出,GraphQL会自动返回错误信息给调用端,你也可以根据业务需求调整为返回自定义的错误格式
  • 监听器触发后立即调用close()关闭,避免重复触发回调导致的逻辑异常

内容的提问来源于stack exchange,提问作者alexB

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.10.05 00:09:03