dispatch_251_pytest.cjs 3.5 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162
  1. // R150 251 langgraph 派发器:kick(分离启动)+ read(单次读日志),不在等待期新建 shell(B-125)
  2. const BASE = process.env.BASE || 'http://192.168.16.251:4096';
  3. const DIR = process.env.DIR || '/sc-data/cfc/cfc-langgraph';
  4. const LOG = process.env.LOG || '/tmp/r150_pytest.log';
  5. const AUTH = 'Basic ' + Buffer.from('opencode:' + (process.env.OPENCODE_SERVER_PASSWORD || 'IwinTrue@123')).toString('base64');
  6. const H = { Authorization: AUTH, 'Content-Type': 'application/json' };
  7. const LOC = '?location%5Bdirectory%5D=' + encodeURIComponent(DIR);
  8. async function j(url, opt) {
  9. try {
  10. const r = await fetch(url, Object.assign({ headers: H }, opt || {}));
  11. const t = await r.text();
  12. try { return { status: r.status, json: JSON.parse(t) }; } catch { return { status: r.status, text: t }; }
  13. } catch (e) { return { status: 0, err: e.message }; }
  14. }
  15. async function sh(cmd, capMs) {
  16. const cr = await j(BASE + '/api/shell' + LOC, { method: 'POST', body: JSON.stringify({ command: cmd, cwd: DIR }) });
  17. const id = cr.json && cr.json.data && cr.json.data.id;
  18. if (!id) return { err: 'create ' + cr.status + ' ' + JSON.stringify(cr).slice(0, 200) };
  19. const t0 = Date.now();
  20. while (Date.now() - t0 < (capMs || 60000)) {
  21. await new Promise(r => setTimeout(r, 2500));
  22. const s = await j(BASE + '/api/shell/' + id + LOC);
  23. const d = (s.json && s.json.data) || null;
  24. if (!d) continue;
  25. if (d.status && d.status !== 'running') {
  26. const o = await j(BASE + '/api/shell/' + id + '/output' + LOC + '&limit=200000');
  27. return { code: d.status, out: (o.json && o.json.data && o.json.data.output) || '' };
  28. }
  29. }
  30. return { err: 'timeout' };
  31. }
  32. (async () => {
  33. const mode = process.argv[2] || 'kick';
  34. // R173 增:LOG 若从本机 env 注入,Git Bash(MSYS) 会把 /tmp/x 改写成 C:/Users/.../x,
  35. // 远端 `tail` 恒报 No such file,且 kick 的重定向也会失败 → 看起来像「日志没生成」。
  36. // 这里直接挡住 Windows 风格路径,强制调用方使用远端 POSIX 路径。
  37. if (LOG.includes(':') || LOG.includes('\\')) {
  38. console.error('BAD_LOG_PATH=' + LOG + ' 请使用远端 POSIX 路径(如 /tmp/rX_pytest.log),不要经由本地 env 注入');
  39. process.exit(2);
  40. }
  41. if (mode === 'kick') {
  42. // R156 修:251 系统 /usr/bin/python 无 pytest,必须用节点 venv(.venv/bin/python → python3.11);
  43. // 退出码另存变量后再写盘(B-126:$? 直接内联在 kick 串里可能落盘失败)。
  44. // R173 复现(B-126 第 6 轮):手工 kick 用 `echo EXIT=$?` 时 186 行日志零 EXIT 行;
  45. // 已在 /tmp/r173_probe.log 验证「先存 rc 再落盘」可写入 → 保留 rc 写法为唯一正确姿势。
  46. const inner = "cd " + DIR + " && .venv/bin/python -m pytest -q --ignore=tests/qna --ignore=tests/test_chat.py > " + LOG + " 2>&1; rc=$?; echo EXIT=$rc >> " + LOG;
  47. const cmd = "rm -f " + LOG + "; setsid nohup bash -c '" + inner + "' >/dev/null 2>&1 & echo KICKED";
  48. const r = await sh(cmd, 40000);
  49. console.log('KICK=' + JSON.stringify(r).slice(0, 400));
  50. const h = await sh('cd ' + DIR + ' && git rev-parse --short HEAD; date', 30000);
  51. console.log('NODE_HEAD=' + ((h.out || h.err || '').trim()));
  52. } else if (mode === 'read') {
  53. const r = await sh('tail -c 4000 ' + LOG, 45000);
  54. console.log(r.err ? 'READ_ERR=' + r.err : r.out);
  55. } else if (mode === 'run') {
  56. const r = await sh(process.argv[3], 120000);
  57. console.log(r.err ? 'ERR=' + r.err : r.out);
  58. }
  59. })();