File size: 2,707 Bytes
63522a5
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
import { describe, expect, it } from "vitest";
import { readdirSync, readFileSync } from "node:fs";
import { join, relative } from "node:path";

const SRC_ROOT = join(process.cwd(), "src");
const EXCLUDED_SEGMENTS = new Set(["mocks", "routeTree.gen.ts"]);
const ALLOWED_AD_HOC_HTTP_FILES = new Set([
  "api/automation-service/automation-service.api.ts",
  "api/cloud/proxy.ts",
  "api/main-app-auth.ts",
]);

function collectSourceFiles(dir: string): string[] {
  return readdirSync(dir, { withFileTypes: true }).flatMap((entry) => {
    const fullPath = join(dir, entry.name);
    // Normalize to forward slashes so the path matches ALLOWED_AD_HOC_HTTP_FILES
    // entries on Windows where path.relative() returns backslash-separated paths.
    const relPath = relative(SRC_ROOT, fullPath).replace(/\\/g, "/");

    if (entry.isDirectory()) {
      if (EXCLUDED_SEGMENTS.has(entry.name)) return [];
      return collectSourceFiles(fullPath);
    }

    if (EXCLUDED_SEGMENTS.has(entry.name)) return [];
    if (!/\.(ts|tsx)$/.test(entry.name)) return [];
    if (/\.(test|spec)\.(ts|tsx)$/.test(entry.name)) return [];
    return [relPath];
  });
}

describe("agent-server API access", () => {
  it("uses typed @openhands/typescript-client access instead of ad-hoc HTTP", () => {
    const violations = collectSourceFiles(SRC_ROOT).flatMap((relPath) => {
      const source = readFileSync(join(SRC_ROOT, relPath), "utf8");
      const fileViolations: string[] = [];

      if (/openHands\s*\./.test(source)) {
        fileViolations.push("uses the shared axios instance directly");
      }

      if (/\bcreateHttpClient\s*\(/.test(source)) {
        fileViolations.push("uses createHttpClient directly");
      }

      if (
        /from\s+["']@openhands\/typescript-client\/client\/http-client["']/.test(
          source,
        )
      ) {
        fileViolations.push("imports the low-level SDK HttpClient directly");
      }

      if (/\bnew\s+HttpClient\s*\(/.test(source)) {
        fileViolations.push("constructs HttpClient directly");
      }

      if (
        (/\baxios\s*\(/.test(source) ||
          /\baxios\s*\.\s*(?:create|get|post|put|patch|delete|request)\s*\(/.test(
            source,
          )) &&
        !ALLOWED_AD_HOC_HTTP_FILES.has(relPath)
      ) {
        fileViolations.push("uses axios directly for HTTP calls");
      }

      if (
        /\bfetch\s*\([\s\S]{0,200}['"`]\/api\//.test(source) &&
        !ALLOWED_AD_HOC_HTTP_FILES.has(relPath)
      ) {
        fileViolations.push("calls an /api path with fetch directly");
      }

      return fileViolations.map((violation) => `${relPath}: ${violation}`);
    });

    expect(violations).toEqual([]);
  });
});