@mandujs/ate 0.17.0 → 0.17.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mandujs/ate",
3
- "version": "0.17.0",
3
+ "version": "0.17.2",
4
4
  "description": "Mandu ATE (Automation Test Engine) - extract/generate/run/report/heal/impact in one package",
5
5
  "type": "module",
6
6
  "sideEffects": false,
package/src/codegen.ts CHANGED
@@ -47,8 +47,8 @@ export function generatePlaywrightSpecs(repoRoot: string, opts?: { onlyRoutes?:
47
47
  let bundle: ScenarioBundle;
48
48
  try {
49
49
  bundle = readJson<ScenarioBundle>(paths.scenariosPath);
50
- } catch (err: any) {
51
- throw new Error(`시나리오 번들 읽기 실패: ${err.message}`);
50
+ } catch (err: unknown) {
51
+ throw new Error(`시나리오 번들 읽기 실패: ${err instanceof Error ? err.message : String(err)}`);
52
52
  }
53
53
 
54
54
  if (!bundle.scenarios || bundle.scenarios.length === 0) {
@@ -59,15 +59,15 @@ export function generatePlaywrightSpecs(repoRoot: string, opts?: { onlyRoutes?:
59
59
  let selectorMap;
60
60
  try {
61
61
  selectorMap = readSelectorMap(repoRoot);
62
- } catch (err: any) {
62
+ } catch (err: unknown) {
63
63
  // Selector map is optional
64
- warnings.push(`Selector map 읽기 실패 (무시): ${err.message}`);
64
+ warnings.push(`Selector map 읽기 실패 (무시): ${err instanceof Error ? err.message : String(err)}`);
65
65
  }
66
66
 
67
67
  try {
68
68
  ensureDir(paths.autoE2eDir);
69
- } catch (err: any) {
70
- throw new Error(`E2E 디렉토리 생성 실패: ${err.message}`);
69
+ } catch (err: unknown) {
70
+ throw new Error(`E2E 디렉토리 생성 실패: ${err instanceof Error ? err.message : String(err)}`);
71
71
  }
72
72
 
73
73
  const files: string[] = [];
@@ -78,40 +78,69 @@ export function generatePlaywrightSpecs(repoRoot: string, opts?: { onlyRoutes?:
78
78
  const safeId = s.id.replace(/[^a-zA-Z0-9_-]/g, "_");
79
79
  const filePath = join(paths.autoE2eDir, `${safeId}.spec.ts`);
80
80
 
81
- const oracle = oracleTemplate(s.oracleLevel, s.route);
82
-
83
- // Generate selector examples if selector map exists
84
- let selectorExamples = "";
85
- if (selectorMap && selectorMap.entries.length > 0) {
86
- const exampleEntry = selectorMap.entries[0];
87
- const locatorChain = buildPlaywrightLocatorChain(exampleEntry);
88
- selectorExamples = ` // Example: Selector with fallback chain\n // const loginBtn = ${locatorChain};\n`;
81
+ let code: string;
82
+
83
+ if (s.kind === "api-smoke") {
84
+ // API route: fetch-based test
85
+ const methods = s.methods ?? ["GET"];
86
+ const testCases = methods.map((method) => {
87
+ return [
88
+ ` test(${JSON.stringify(`${method} ${s.route}`)}, async ({ baseURL }) => {`,
89
+ ` const url = (baseURL ?? "http://localhost:3333") + ${JSON.stringify(s.route)};`,
90
+ ` const res = await fetch(url, { method: ${JSON.stringify(method)} });`,
91
+ ` expect(res.status).toBeLessThan(500);`,
92
+ ` expect(res.headers.get("content-type")).toBeTruthy();`,
93
+ method === "GET" ? ` const body = await res.text();\n expect(body.length).toBeGreaterThan(0);` : "",
94
+ ` });`,
95
+ ].filter(Boolean).join("\n");
96
+ });
97
+
98
+ code = [
99
+ specHeader(),
100
+ `test.describe(${JSON.stringify(s.id)}, () => {`,
101
+ ...testCases,
102
+ `});`,
103
+ "",
104
+ ].join("\n");
105
+ } else {
106
+ // Page route: browser-based test
107
+ const oracle = oracleTemplate(s.oracleLevel, s.route);
108
+
109
+ // Generate selector examples if selector map exists
110
+ let selectorExamples = "";
111
+ if (selectorMap && selectorMap.entries.length > 0) {
112
+ const exampleEntry = selectorMap.entries[0];
113
+ const locatorChain = buildPlaywrightLocatorChain(exampleEntry);
114
+ selectorExamples = ` // Example: Selector with fallback chain\n // const loginBtn = ${locatorChain};\n`;
115
+ }
116
+
117
+ code = [
118
+ specHeader(),
119
+ `test.describe(${JSON.stringify(s.id)}, () => {`,
120
+ ` test(${JSON.stringify(`smoke ${s.route}`)}, async ({ page, baseURL }) => {`,
121
+ ` const url = (baseURL ?? "http://localhost:3333") + ${JSON.stringify(s.route === "/" ? "/" : s.route)};`,
122
+ ` ${oracle.setup.split("\n").join("\n ")}`,
123
+ ` await page.goto(url);`,
124
+ selectorExamples,
125
+ ` ${oracle.assertions.split("\n").join("\n ")}`,
126
+ ` });`,
127
+ `});`,
128
+ "",
129
+ ].join("\n");
89
130
  }
90
131
 
91
- const code = [
92
- specHeader(),
93
- `test.describe(${JSON.stringify(s.id)}, () => {`,
94
- ` test(${JSON.stringify(`smoke ${s.route}`)}, async ({ page, baseURL }) => {`,
95
- ` const url = (baseURL ?? "http://localhost:3333") + ${JSON.stringify(s.route === "/" ? "/" : s.route)};`,
96
- ` ${oracle.setup.split("\n").join("\n ")}`,
97
- ` await page.goto(url);`,
98
- selectorExamples,
99
- ` ${oracle.assertions.split("\n").join("\n ")}`,
100
- ` });`,
101
- `});`,
102
- "",
103
- ].join("\n");
104
-
105
132
  try {
106
133
  writeFileSync(filePath, code, "utf8");
107
134
  files.push(filePath);
108
- } catch (err: any) {
109
- warnings.push(`Spec 파일 쓰기 실패 (${filePath}): ${err.message}`);
110
- console.error(`[ATE] Spec 생성 실패: ${filePath} - ${err.message}`);
135
+ } catch (err: unknown) {
136
+ const msg = err instanceof Error ? err.message : String(err);
137
+ warnings.push(`Spec 파일 쓰기 실패 (${filePath}): ${msg}`);
138
+ console.error(`[ATE] Spec 생성 실패: ${filePath} - ${msg}`);
111
139
  }
112
- } catch (err: any) {
113
- warnings.push(`Spec 생성 실패 (${s.id}): ${err.message}`);
114
- console.error(`[ATE] Spec 생성 에러: ${s.id} - ${err.message}`);
140
+ } catch (err: unknown) {
141
+ const msg = err instanceof Error ? err.message : String(err);
142
+ warnings.push(`Spec 생성 실패 (${s.id}): ${msg}`);
143
+ console.error(`[ATE] Spec 생성 에러: ${s.id} - ${msg}`);
115
144
  // Continue with next scenario
116
145
  }
117
146
  }
@@ -131,9 +160,10 @@ export function generatePlaywrightSpecs(repoRoot: string, opts?: { onlyRoutes?:
131
160
  Bun.write(configPath, desiredConfig);
132
161
  }
133
162
  }
134
- } catch (err: any) {
135
- warnings.push(`Playwright config 생성 실패: ${err.message}`);
136
- console.warn(`[ATE] Playwright config 생성 실패: ${err.message}`);
163
+ } catch (err: unknown) {
164
+ const msg = err instanceof Error ? err.message : String(err);
165
+ warnings.push(`Playwright config 생성 실패: ${msg}`);
166
+ console.warn(`[ATE] Playwright config 생성 실패: ${msg}`);
137
167
  }
138
168
 
139
169
  return { files, warnings };
package/src/dep-graph.ts CHANGED
@@ -38,10 +38,10 @@ function normalizePath(path: string, rootDir: string): string {
38
38
  * Resolve import specifier to file path
39
39
  */
40
40
  function resolveImport(
41
- sourceFile: any, // ts-morph SourceFile (lazy loaded)
41
+ sourceFile: import("./ts-morph-types").SourceFile,
42
42
  importSpecifier: string,
43
43
  rootDir: string,
44
- project: any // ts-morph Project (lazy loaded)
44
+ project: import("./ts-morph-types").Project
45
45
  ): string | null {
46
46
  // Skip external modules (no relative/absolute path)
47
47
  if (!importSpecifier.startsWith(".") && !importSpecifier.startsWith("/")) {
package/src/extractor.ts CHANGED
@@ -3,18 +3,24 @@ import { relative, join } from "node:path";
3
3
  import { createEmptyGraph, addEdge, addNode } from "./ir";
4
4
  import { getAtePaths, writeJson } from "./fs";
5
5
  import type { ExtractInput, InteractionGraph } from "./types";
6
+ import type { Node, CallExpression, JsxAttribute, SyntaxKindEnum } from "./ts-morph-types";
6
7
 
7
- const DEFAULT_ROUTE_GLOBS = ["app/**/page.tsx", "routes/**/page.tsx"]; // demo-first default
8
+ const DEFAULT_ROUTE_GLOBS = [
9
+ "app/**/page.tsx",
10
+ "app/**/route.ts",
11
+ "routes/**/page.tsx",
12
+ "routes/**/route.ts",
13
+ ];
8
14
 
9
- function isStringLiteral(node: any /* ts-morph Node */, SyntaxKind: any): boolean {
10
- return node.getKind() === SyntaxKind.StringLiteral;
15
+ function isStringLiteral(node: Node, SK: SyntaxKindEnum): boolean {
16
+ return node.getKind() === SK.StringLiteral;
11
17
  }
12
18
 
13
- function tryExtractLiteralArg(callExpr: any /* ts-morph CallExpression */, argIndex = 0, SyntaxKind: any): string | null {
19
+ function tryExtractLiteralArg(callExpr: CallExpression, argIndex = 0, SK: SyntaxKindEnum): string | null {
14
20
  const args = callExpr.getArguments();
15
21
  const arg = args[argIndex];
16
22
  if (!arg) return null;
17
- if (isStringLiteral(arg, SyntaxKind)) return arg.getLiteralValue();
23
+ if (isStringLiteral(arg, SK)) return (arg as Node & { getLiteralValue(): string }).getLiteralValue();
18
24
  return null;
19
25
  }
20
26
 
@@ -41,8 +47,8 @@ export async function extract(input: ExtractInput): Promise<{ ok: true; graphPat
41
47
  onlyFiles: true,
42
48
  ignore: ["**/node_modules/**", "**/.mandu/**"],
43
49
  });
44
- } catch (err: any) {
45
- throw new Error(`파일 검색 실패: ${err.message}`);
50
+ } catch (err: unknown) {
51
+ throw new Error(`파일 검색 실패: ${err instanceof Error ? err.message : String(err)}`);
46
52
  }
47
53
 
48
54
  if (routeFiles.length === 0) {
@@ -51,15 +57,16 @@ export async function extract(input: ExtractInput): Promise<{ ok: true; graphPat
51
57
 
52
58
  // Lazy load ts-morph only when needed
53
59
  const { Project, SyntaxKind } = await import("ts-morph");
60
+ const SK = SyntaxKind as unknown as SyntaxKindEnum;
54
61
 
55
- let project: any; // ts-morph Project
62
+ let project: InstanceType<typeof Project>;
56
63
  try {
57
64
  project = new Project({
58
65
  tsConfigFilePath: input.tsconfigPath ? join(repoRoot, input.tsconfigPath) : undefined,
59
66
  skipAddingFilesFromTsConfig: true,
60
67
  });
61
- } catch (err: any) {
62
- throw new Error(`TypeScript 프로젝트 초기화 실패: ${err.message}`);
68
+ } catch (err: unknown) {
69
+ throw new Error(`TypeScript 프로젝트 초기화 실패: ${err instanceof Error ? err.message : String(err)}`);
63
70
  }
64
71
 
65
72
  for (const filePath of routeFiles) {
@@ -68,86 +75,112 @@ export async function extract(input: ExtractInput): Promise<{ ok: true; graphPat
68
75
  const rel = relative(repoRoot, filePath);
69
76
  const relNormalized = rel.replace(/\\/g, "/");
70
77
 
71
- // route node id: normalize to path without trailing /page.tsx
78
+ const isApiRoute = relNormalized.endsWith("/route.ts");
79
+
80
+ // route node id: normalize to path without trailing /page.tsx or /route.ts
72
81
  const routePath = relNormalized
73
82
  .replace(/^app\//, "/")
74
83
  .replace(/^routes\//, "/")
75
84
  .replace(/\/page\.tsx$/, "")
85
+ .replace(/\/route\.ts$/, "")
76
86
  .replace(/\/index\.tsx$/, "")
77
87
  .replace(/\/page$/, "")
78
88
  .replace(/\\/g, "/");
79
89
 
80
- addNode(graph, { kind: "route", id: routePath === "" ? "/" : routePath, file: relNormalized, path: routePath === "" ? "/" : routePath });
90
+ // API route: extract HTTP methods from exports (GET, POST, PUT, PATCH, DELETE)
91
+ let methods: string[] = [];
92
+ if (isApiRoute) {
93
+ const HTTP_METHODS = ["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"];
94
+ const exportDecls = sourceFile.getExportedDeclarations();
95
+ for (const [name] of exportDecls) {
96
+ if (HTTP_METHODS.includes(name)) {
97
+ methods.push(name);
98
+ }
99
+ }
100
+ if (methods.length === 0) methods = ["GET"]; // default
101
+ }
102
+
103
+ addNode(graph, {
104
+ kind: "route",
105
+ id: routePath === "" ? "/" : routePath,
106
+ file: relNormalized,
107
+ path: routePath === "" ? "/" : routePath,
108
+ ...(isApiRoute ? { methods } : {}),
109
+ });
110
+
111
+ // API route에는 JSX/navigation이 없으므로 건너뜀
112
+ if (isApiRoute) continue;
81
113
 
82
114
  // ManduLink / Link literal extraction: <Link href="/x"> or <ManduLink to="/x">
83
115
  try {
84
- const jsxAttrs = sourceFile.getDescendantsOfKind(SyntaxKind.JsxAttribute);
116
+ const jsxAttrs = sourceFile.getDescendantsOfKind(SyntaxKind.JsxAttribute) as unknown as JsxAttribute[];
85
117
  for (const attr of jsxAttrs) {
86
118
  try {
87
- const name = (attr as any).getNameNode?.().getText?.() ?? (attr as any).getName?.() ?? "";
119
+ const name = attr.getNameNode?.().getText?.() ?? attr.getName?.() ?? "";
88
120
  if (name !== "to" && name !== "href") continue;
89
- const init = (attr as any).getInitializer?.();
121
+ const init = attr.getInitializer?.();
90
122
  if (!init) continue;
91
- if (init.getKind?.() === SyntaxKind.StringLiteral) {
92
- const raw = (init as any).getLiteralValue?.() ?? init.getText?.();
123
+ if (init.getKind?.() === SK.StringLiteral) {
124
+ const raw = init.getLiteralValue?.() ?? init.getText?.();
93
125
  const to = typeof raw === "string" ? raw.replace(/^"|"$/g, "") : null;
94
126
  if (typeof to === "string" && to.startsWith("/")) {
95
127
  addEdge(graph, { kind: "navigate", from: routePath || "/", to, file: relNormalized, source: `<jsx ${name}>` });
96
128
  }
97
129
  }
98
- } catch (err: any) {
130
+ } catch (err: unknown) {
99
131
  // Skip invalid JSX attributes
100
- warnings.push(`JSX 속성 파싱 실패 (${relNormalized}): ${err.message}`);
132
+ warnings.push(`JSX 속성 파싱 실패 (${relNormalized}): ${err instanceof Error ? err.message : String(err)}`);
101
133
  }
102
134
  }
103
- } catch (err: any) {
104
- warnings.push(`JSX 분석 실패 (${relNormalized}): ${err.message}`);
135
+ } catch (err: unknown) {
136
+ warnings.push(`JSX 분석 실패 (${relNormalized}): ${err instanceof Error ? err.message : String(err)}`);
105
137
  }
106
138
 
107
139
  // mandu.navigate("/x") literal
108
140
  try {
109
- const calls = sourceFile.getDescendantsOfKind(SyntaxKind.CallExpression);
141
+ const calls = sourceFile.getDescendantsOfKind(SyntaxKind.CallExpression) as unknown as CallExpression[];
110
142
  for (const call of calls) {
111
143
  try {
112
144
  const exprText = call.getExpression().getText();
113
145
  if (exprText === "mandu.navigate" || exprText.endsWith(".navigate")) {
114
- const to = tryExtractLiteralArg(call, 0, SyntaxKind);
146
+ const to = tryExtractLiteralArg(call, 0, SK);
115
147
  if (to && to.startsWith("/")) {
116
148
  addEdge(graph, { kind: "navigate", from: routePath || "/", to, file: relNormalized, source: "mandu.navigate" });
117
149
  }
118
150
  }
119
151
  if (exprText === "mandu.modal.open" || exprText.endsWith(".modal.open")) {
120
- const modal = tryExtractLiteralArg(call, 0, SyntaxKind);
152
+ const modal = tryExtractLiteralArg(call, 0, SK);
121
153
  if (modal) {
122
154
  addEdge(graph, { kind: "openModal", from: routePath || "/", modal, file: relNormalized, source: "mandu.modal.open" });
123
155
  }
124
156
  }
125
157
  if (exprText === "mandu.action.run" || exprText.endsWith(".action.run")) {
126
- const action = tryExtractLiteralArg(call, 0, SyntaxKind);
158
+ const action = tryExtractLiteralArg(call, 0, SK);
127
159
  if (action) {
128
160
  addEdge(graph, { kind: "runAction", from: routePath || "/", action, file: relNormalized, source: "mandu.action.run" });
129
161
  }
130
162
  }
131
- } catch (err: any) {
163
+ } catch (err: unknown) {
132
164
  // Skip invalid call expressions
133
- warnings.push(`함수 호출 파싱 실패 (${relNormalized}): ${err.message}`);
165
+ warnings.push(`함수 호출 파싱 실패 (${relNormalized}): ${err instanceof Error ? err.message : String(err)}`);
134
166
  }
135
167
  }
136
- } catch (err: any) {
137
- warnings.push(`함수 호출 분석 실패 (${relNormalized}): ${err.message}`);
168
+ } catch (err: unknown) {
169
+ warnings.push(`함수 호출 분석 실패 (${relNormalized}): ${err instanceof Error ? err.message : String(err)}`);
138
170
  }
139
- } catch (err: any) {
171
+ } catch (err: unknown) {
140
172
  // Graceful degradation: skip this file and continue
141
- warnings.push(`파일 파싱 실패 (${filePath}): ${err.message}`);
142
- console.warn(`[ATE] 파일 스킵: ${filePath} - ${err.message}`);
173
+ const msg = err instanceof Error ? err.message : String(err);
174
+ warnings.push(`파일 파싱 실패 (${filePath}): ${msg}`);
175
+ console.warn(`[ATE] 파일 스킵: ${filePath} - ${msg}`);
143
176
  continue;
144
177
  }
145
178
  }
146
179
 
147
180
  try {
148
181
  writeJson(paths.interactionGraphPath, graph);
149
- } catch (err: any) {
150
- throw new Error(`Interaction graph 저장 실패: ${err.message}`);
182
+ } catch (err: unknown) {
183
+ throw new Error(`Interaction graph 저장 실패: ${err instanceof Error ? err.message : String(err)}`);
151
184
  }
152
185
 
153
186
  return {
package/src/fs.ts CHANGED
@@ -34,15 +34,17 @@ export function getAtePaths(repoRoot: string): AtePaths {
34
34
  export function ensureDir(path: string): void {
35
35
  try {
36
36
  mkdirSync(path, { recursive: true });
37
- } catch (err: any) {
38
- if (err.code === "EACCES") {
37
+ } catch (err: unknown) {
38
+ const isNodeError = err instanceof Error && "code" in err;
39
+ const code = isNodeError ? (err as NodeJS.ErrnoException).code : undefined;
40
+ if (code === "EACCES") {
39
41
  throw new ATEFileError(
40
42
  `디렉토리 생성 권한이 없습니다: ${path}`,
41
43
  "PERMISSION_DENIED",
42
44
  path,
43
45
  );
44
46
  }
45
- if (err.code === "ENOSPC") {
47
+ if (code === "ENOSPC") {
46
48
  throw new ATEFileError(
47
49
  `디스크 공간이 부족합니다: ${path}`,
48
50
  "NO_SPACE",
@@ -50,8 +52,8 @@ export function ensureDir(path: string): void {
50
52
  );
51
53
  }
52
54
  throw new ATEFileError(
53
- `디렉토리 생성 실패: ${path} (${err.message})`,
54
- err.code || "UNKNOWN",
55
+ `디렉토리 생성 실패: ${path} (${err instanceof Error ? err.message : String(err)})`,
56
+ code || "UNKNOWN",
55
57
  path,
56
58
  );
57
59
  }
@@ -65,18 +67,20 @@ export function writeJson(path: string, data: unknown): void {
65
67
  try {
66
68
  ensureDir(dirname(path));
67
69
  writeFileSync(path, JSON.stringify(data, null, 2), "utf8");
68
- } catch (err: any) {
70
+ } catch (err: unknown) {
69
71
  if (err instanceof ATEFileError) {
70
72
  throw err;
71
73
  }
72
- if (err.code === "EACCES") {
74
+ const isNodeError = err instanceof Error && "code" in err;
75
+ const code = isNodeError ? (err as NodeJS.ErrnoException).code : undefined;
76
+ if (code === "EACCES") {
73
77
  throw new ATEFileError(
74
78
  `파일 쓰기 권한이 없습니다: ${path}`,
75
79
  "PERMISSION_DENIED",
76
80
  path,
77
81
  );
78
82
  }
79
- if (err.code === "ENOSPC") {
83
+ if (code === "ENOSPC") {
80
84
  throw new ATEFileError(
81
85
  `디스크 공간이 부족합니다: ${path}`,
82
86
  "NO_SPACE",
@@ -84,8 +88,8 @@ export function writeJson(path: string, data: unknown): void {
84
88
  );
85
89
  }
86
90
  throw new ATEFileError(
87
- `JSON 파일 쓰기 실패: ${path} (${err.message})`,
88
- err.code || "UNKNOWN",
91
+ `JSON 파일 쓰기 실패: ${path} (${err instanceof Error ? err.message : String(err)})`,
92
+ code || "UNKNOWN",
89
93
  path,
90
94
  );
91
95
  }
@@ -107,11 +111,13 @@ export function readJson<T>(path: string): T {
107
111
 
108
112
  const content = readFileSync(path, "utf8");
109
113
  return JSON.parse(content) as T;
110
- } catch (err: any) {
114
+ } catch (err: unknown) {
111
115
  if (err instanceof ATEFileError) {
112
116
  throw err;
113
117
  }
114
- if (err.code === "EACCES") {
118
+ const isNodeError = err instanceof Error && "code" in err;
119
+ const code = isNodeError ? (err as NodeJS.ErrnoException).code : undefined;
120
+ if (code === "EACCES") {
115
121
  throw new ATEFileError(
116
122
  `파일 읽기 권한이 없습니다: ${path}`,
117
123
  "PERMISSION_DENIED",
@@ -126,8 +132,8 @@ export function readJson<T>(path: string): T {
126
132
  );
127
133
  }
128
134
  throw new ATEFileError(
129
- `JSON 파일 읽기 실패: ${path} (${err.message})`,
130
- err.code || "UNKNOWN",
135
+ `JSON 파일 읽기 실패: ${path} (${err instanceof Error ? err.message : String(err)})`,
136
+ code || "UNKNOWN",
131
137
  path,
132
138
  );
133
139
  }
package/src/impact.ts CHANGED
@@ -39,14 +39,14 @@ export async function computeImpact(input: ImpactInput): Promise<{ changedFiles:
39
39
  // Verify git revisions
40
40
  try {
41
41
  verifyGitRev(repoRoot, base);
42
- } catch (err: any) {
43
- throw new Error(`잘못된 base revision: ${base} (${err.message})`);
42
+ } catch (err: unknown) {
43
+ throw new Error(`잘못된 base revision: ${base} (${err instanceof Error ? err.message : String(err)})`);
44
44
  }
45
45
 
46
46
  try {
47
47
  verifyGitRev(repoRoot, head);
48
- } catch (err: any) {
49
- throw new Error(`잘못된 head revision: ${head} (${err.message})`);
48
+ } catch (err: unknown) {
49
+ throw new Error(`잘못된 head revision: ${head} (${err instanceof Error ? err.message : String(err)})`);
50
50
  }
51
51
 
52
52
  let out: string;
@@ -55,8 +55,8 @@ export async function computeImpact(input: ImpactInput): Promise<{ changedFiles:
55
55
  cwd: repoRoot,
56
56
  stdio: ["ignore", "pipe", "pipe"],
57
57
  }).toString("utf8");
58
- } catch (err: any) {
59
- throw new Error(`Git diff 실행 실패: ${err.message}`);
58
+ } catch (err: unknown) {
59
+ throw new Error(`Git diff 실행 실패: ${err instanceof Error ? err.message : String(err)}`);
60
60
  }
61
61
 
62
62
  const changedFiles = out.split("\n").map((s) => toPosixPath(s.trim())).filter(Boolean);
@@ -71,8 +71,8 @@ export async function computeImpact(input: ImpactInput): Promise<{ changedFiles:
71
71
  let graph: InteractionGraph;
72
72
  try {
73
73
  graph = readJson<InteractionGraph>(paths.interactionGraphPath);
74
- } catch (err: any) {
75
- throw new Error(`Interaction graph 읽기 실패: ${err.message}`);
74
+ } catch (err: unknown) {
75
+ throw new Error(`Interaction graph 읽기 실패: ${err instanceof Error ? err.message : String(err)}`);
76
76
  }
77
77
 
78
78
  if (!graph.nodes || graph.nodes.length === 0) {
@@ -95,8 +95,8 @@ export async function computeImpact(input: ImpactInput): Promise<{ changedFiles:
95
95
  include: ["**/*.ts", "**/*.tsx", "**/*.js", "**/*.jsx"],
96
96
  exclude: ["**/node_modules/**", "**/*.test.ts", "**/*.spec.ts"],
97
97
  });
98
- } catch (err: any) {
99
- warnings.push(`Dependency graph 빌드 실패: ${err.message}`);
98
+ } catch (err: unknown) {
99
+ warnings.push(`Dependency graph 빌드 실패: ${err instanceof Error ? err.message : String(err)}`);
100
100
  // Fallback: only direct file matching
101
101
  const selected = new Set<string>();
102
102
  for (const changedFile of changedFiles) {
@@ -136,8 +136,8 @@ export async function computeImpact(input: ImpactInput): Promise<{ changedFiles:
136
136
  }
137
137
  }
138
138
  }
139
- } catch (err: any) {
140
- warnings.push(`파일 영향 분석 실패 (${changedFile}): ${err.message}`);
139
+ } catch (err: unknown) {
140
+ warnings.push(`파일 영향 분석 실패 (${changedFile}): ${err instanceof Error ? err.message : String(err)}`);
141
141
  // Continue with next file
142
142
  }
143
143
  }
package/src/pipeline.ts CHANGED
@@ -57,9 +57,10 @@ export async function runFullPipeline(options: AutoPipelineOptions): Promise<Aut
57
57
  });
58
58
  result.steps.extract.ok = true;
59
59
  console.log("✅ [ATE Pipeline] Extract 완료");
60
- } catch (err: any) {
61
- result.steps.extract.error = err.message;
62
- console.error(`❌ [ATE Pipeline] Extract 실패: ${err.message}`);
60
+ } catch (err: unknown) {
61
+ const msg = err instanceof Error ? err.message : String(err);
62
+ result.steps.extract.error = msg;
63
+ console.error(`❌ [ATE Pipeline] Extract 실패: ${msg}`);
63
64
  return result;
64
65
  }
65
66
 
@@ -72,9 +73,10 @@ export async function runFullPipeline(options: AutoPipelineOptions): Promise<Aut
72
73
  });
73
74
  result.steps.generate.ok = true;
74
75
  console.log("✅ [ATE Pipeline] Generate 완료");
75
- } catch (err: any) {
76
- result.steps.generate.error = err.message;
77
- console.error(`❌ [ATE Pipeline] Generate 실패: ${err.message}`);
76
+ } catch (err: unknown) {
77
+ const msg = err instanceof Error ? err.message : String(err);
78
+ result.steps.generate.error = msg;
79
+ console.error(`❌ [ATE Pipeline] Generate 실패: ${msg}`);
78
80
  return result;
79
81
  }
80
82
 
@@ -100,14 +102,15 @@ export async function runFullPipeline(options: AutoPipelineOptions): Promise<Aut
100
102
  console.log(
101
103
  `✅ [ATE Pipeline] Impact Analysis 완료 - ${impact.selectedRoutes.length}개 라우트 선택됨`,
102
104
  );
103
- } catch (err: any) {
105
+ } catch (err: unknown) {
106
+ const msg = err instanceof Error ? err.message : String(err);
104
107
  result.steps.impact = {
105
108
  ok: false,
106
109
  mode: "full",
107
110
  selectedRoutes: [],
108
- error: err.message,
111
+ error: msg,
109
112
  };
110
- console.warn(`⚠️ [ATE Pipeline] Impact Analysis 실패, 전체 테스트 실행: ${err.message}`);
113
+ console.warn(`⚠️ [ATE Pipeline] Impact Analysis 실패, 전체 테스트 실행: ${msg}`);
111
114
  // Impact analysis 실패 시에도 계속 진행 (full test)
112
115
  }
113
116
  }
@@ -132,9 +135,10 @@ export async function runFullPipeline(options: AutoPipelineOptions): Promise<Aut
132
135
  console.log(
133
136
  `${exitCode === 0 ? "✅" : "⚠️"} [ATE Pipeline] Run 완료 - exitCode: ${exitCode}`,
134
137
  );
135
- } catch (err: any) {
136
- result.steps.run.error = err.message;
137
- console.error(`❌ [ATE Pipeline] Run 실패: ${err.message}`);
138
+ } catch (err: unknown) {
139
+ const msg = err instanceof Error ? err.message : String(err);
140
+ result.steps.run.error = msg;
141
+ console.error(`❌ [ATE Pipeline] Run 실패: ${msg}`);
138
142
  return result;
139
143
  }
140
144
 
@@ -158,9 +162,10 @@ export async function runFullPipeline(options: AutoPipelineOptions): Promise<Aut
158
162
  });
159
163
  result.steps.report = { ok: true, summaryPath: reportResult.summaryPath };
160
164
  console.log(`✅ [ATE Pipeline] Report 완료 - ${reportResult.summaryPath}`);
161
- } catch (err: any) {
162
- result.steps.report.error = err.message;
163
- console.error(`❌ [ATE Pipeline] Report 실패: ${err.message}`);
165
+ } catch (err: unknown) {
166
+ const msg = err instanceof Error ? err.message : String(err);
167
+ result.steps.report.error = msg;
168
+ console.error(`❌ [ATE Pipeline] Report 실패: ${msg}`);
164
169
  return result;
165
170
  }
166
171
 
@@ -179,13 +184,14 @@ export async function runFullPipeline(options: AutoPipelineOptions): Promise<Aut
179
184
  console.log(
180
185
  `✅ [ATE Pipeline] Heal 완료 - ${healResult.suggestions?.length ?? 0}개 제안 생성됨`,
181
186
  );
182
- } catch (err: any) {
187
+ } catch (err: unknown) {
188
+ const msg = err instanceof Error ? err.message : String(err);
183
189
  result.steps.heal = {
184
190
  ok: false,
185
191
  suggestionsCount: 0,
186
- error: err.message,
192
+ error: msg,
187
193
  };
188
- console.warn(`⚠️ [ATE Pipeline] Heal 실패: ${err.message}`);
194
+ console.warn(`⚠️ [ATE Pipeline] Heal 실패: ${msg}`);
189
195
  // Heal 실패는 전체 파이프라인 실패로 보지 않음
190
196
  }
191
197
  }
@@ -197,9 +203,9 @@ export async function runFullPipeline(options: AutoPipelineOptions): Promise<Aut
197
203
  );
198
204
 
199
205
  return result;
200
- } catch (err: any) {
206
+ } catch (err: unknown) {
201
207
  throw new ATEFileError(
202
- `파이프라인 실행 중 예상치 못한 오류: ${err.message}`,
208
+ `파이프라인 실행 중 예상치 못한 오류: ${err instanceof Error ? err.message : String(err)}`,
203
209
  "PIPELINE_ERROR",
204
210
  options.repoRoot,
205
211
  );
package/src/report.ts CHANGED
@@ -27,8 +27,8 @@ export function composeSummary(params: {
27
27
  let oracle;
28
28
  try {
29
29
  oracle = createDefaultOracle(params.oracleLevel);
30
- } catch (err: any) {
31
- throw new Error(`Oracle 생성 실패: ${err.message}`);
30
+ } catch (err: unknown) {
31
+ throw new Error(`Oracle 생성 실패: ${err instanceof Error ? err.message : String(err)}`);
32
32
  }
33
33
 
34
34
  return {
@@ -74,16 +74,16 @@ export function writeSummary(repoRoot: string, runId: string, summary: SummaryJs
74
74
 
75
75
  try {
76
76
  ensureDir(runDir);
77
- } catch (err: any) {
78
- throw new Error(`Report 디렉토리 생성 실패: ${err.message}`);
77
+ } catch (err: unknown) {
78
+ throw new Error(`Report 디렉토리 생성 실패: ${err instanceof Error ? err.message : String(err)}`);
79
79
  }
80
80
 
81
81
  const outPath = join(runDir, "summary.json");
82
82
 
83
83
  try {
84
84
  writeJson(outPath, summary);
85
- } catch (err: any) {
86
- throw new Error(`Summary 파일 저장 실패: ${err.message}`);
85
+ } catch (err: unknown) {
86
+ throw new Error(`Summary 파일 저장 실패: ${err instanceof Error ? err.message : String(err)}`);
87
87
  }
88
88
 
89
89
  return outPath;
@@ -120,8 +120,8 @@ export async function generateReport(options: GenerateReportOptions): Promise<{
120
120
  includeTraces,
121
121
  });
122
122
  result.html = htmlResult.path;
123
- } catch (err: any) {
124
- throw new Error(`HTML 리포트 생성 실패: ${err.message}`);
123
+ } catch (err: unknown) {
124
+ throw new Error(`HTML 리포트 생성 실패: ${err instanceof Error ? err.message : String(err)}`);
125
125
  }
126
126
  }
127
127
 
@@ -40,8 +40,8 @@ export async function generateHtmlReport(options: HtmlReportOptions): Promise<Ht
40
40
  try {
41
41
  const content = readFileSync(summaryPath, "utf-8");
42
42
  summary = JSON.parse(content);
43
- } catch (err: any) {
44
- throw new Error(`Summary 파일 읽기 실패: ${err.message}`);
43
+ } catch (err: unknown) {
44
+ throw new Error(`Summary 파일 읽기 실패: ${err instanceof Error ? err.message : String(err)}`);
45
45
  }
46
46
 
47
47
  // 2. 스크린샷 URL 수집 (선택)
@@ -57,8 +57,8 @@ export async function generateHtmlReport(options: HtmlReportOptions): Promise<Ht
57
57
  screenshotUrls.push(`./screenshots/${f}`);
58
58
  });
59
59
  }
60
- } catch (err: any) {
61
- console.warn(`스크린샷 수집 실패: ${err.message}`);
60
+ } catch (err: unknown) {
61
+ console.warn(`스크린샷 수집 실패: ${err instanceof Error ? err.message : String(err)}`);
62
62
  }
63
63
  }
64
64
 
@@ -70,8 +70,8 @@ export async function generateHtmlReport(options: HtmlReportOptions): Promise<Ht
70
70
  try {
71
71
  ensureDir(join(htmlPath, "..")); // 상위 디렉토리 확인
72
72
  writeFileSync(htmlPath, html, "utf-8");
73
- } catch (err: any) {
74
- throw new Error(`HTML 파일 저장 실패: ${err.message}`);
73
+ } catch (err: unknown) {
74
+ throw new Error(`HTML 파일 저장 실패: ${err instanceof Error ? err.message : String(err)}`);
75
75
  }
76
76
 
77
77
  const size = Buffer.byteLength(html, "utf-8");
package/src/runner.ts CHANGED
@@ -31,8 +31,8 @@ export async function runPlaywright(input: RunInput): Promise<RunResult> {
31
31
  try {
32
32
  ensureDir(runDir);
33
33
  ensureDir(latestDir);
34
- } catch (err: any) {
35
- throw new Error(`Report 디렉토리 생성 실패: ${err.message}`);
34
+ } catch (err: unknown) {
35
+ throw new Error(`Report 디렉토리 생성 실패: ${err instanceof Error ? err.message : String(err)}`);
36
36
  }
37
37
 
38
38
  const baseURL = input.baseURL ?? process.env.BASE_URL ?? "http://localhost:3333";
@@ -57,8 +57,8 @@ export async function runPlaywright(input: RunInput): Promise<RunResult> {
57
57
  stdio: "inherit",
58
58
  env,
59
59
  });
60
- } catch (err: any) {
61
- throw new Error(`Playwright 프로세스 시작 실패: ${err.message}`);
60
+ } catch (err: unknown) {
61
+ throw new Error(`Playwright 프로세스 시작 실패: ${err instanceof Error ? err.message : String(err)}`);
62
62
  }
63
63
 
64
64
  const exitCode: number = await new Promise((resolve, reject) => {
@@ -91,8 +91,8 @@ export async function runPlaywright(input: RunInput): Promise<RunResult> {
91
91
  // record minimal run metadata
92
92
  try {
93
93
  writeJson(join(runDir, "run.json"), { ...result, baseURL, at: new Date().toISOString() });
94
- } catch (err: any) {
95
- console.warn(`[ATE] Run metadata 저장 실패: ${err.message}`);
94
+ } catch (err: unknown) {
95
+ console.warn(`[ATE] Run metadata 저장 실패: ${err instanceof Error ? err.message : String(err)}`);
96
96
  // Non-fatal: continue
97
97
  }
98
98
 
package/src/scenario.ts CHANGED
@@ -3,8 +3,9 @@ import { getAtePaths, readJson, writeJson } from "./fs";
3
3
 
4
4
  export interface GeneratedScenario {
5
5
  id: string;
6
- kind: "route-smoke";
6
+ kind: "route-smoke" | "api-smoke";
7
7
  route: string;
8
+ methods?: string[];
8
9
  oracleLevel: OracleLevel;
9
10
  }
10
11
 
@@ -28,18 +29,22 @@ export function generateScenariosFromGraph(graph: InteractionGraph, oracleLevel:
28
29
  throw new Error("빈 interaction graph입니다 (nodes가 없습니다)");
29
30
  }
30
31
 
31
- const routes = graph.nodes.filter((n) => n.kind === "route") as Array<{ kind: "route"; id: string; path: string }>;
32
+ const routes = graph.nodes.filter((n) => n.kind === "route") as Array<{ kind: "route"; id: string; path: string; methods?: string[] }>;
32
33
 
33
34
  if (routes.length === 0) {
34
35
  console.warn("[ATE] 경고: route가 없습니다. 빈 시나리오 번들을 생성합니다.");
35
36
  }
36
37
 
37
- const scenarios: GeneratedScenario[] = routes.map((r) => ({
38
- id: `route:${r.id}`,
39
- kind: "route-smoke",
40
- route: r.id,
41
- oracleLevel,
42
- }));
38
+ const scenarios: GeneratedScenario[] = routes.map((r) => {
39
+ const isApi = r.path.startsWith("/api/") || (r.methods && r.methods.length > 0);
40
+ return {
41
+ id: `${isApi ? "api" : "route"}:${r.id}`,
42
+ kind: isApi ? "api-smoke" : "route-smoke",
43
+ route: r.id,
44
+ ...(isApi && r.methods ? { methods: r.methods } : {}),
45
+ oracleLevel,
46
+ };
47
+ });
43
48
 
44
49
  return {
45
50
  schemaVersion: 1,
@@ -55,16 +60,16 @@ export function generateAndWriteScenarios(repoRoot: string, oracleLevel: OracleL
55
60
  let graph: InteractionGraph;
56
61
  try {
57
62
  graph = readJson<InteractionGraph>(paths.interactionGraphPath);
58
- } catch (err: any) {
59
- throw new Error(`Interaction graph 읽기 실패: ${err.message}`);
63
+ } catch (err: unknown) {
64
+ throw new Error(`Interaction graph 읽기 실패: ${err instanceof Error ? err.message : String(err)}`);
60
65
  }
61
66
 
62
67
  const bundle = generateScenariosFromGraph(graph, oracleLevel);
63
68
 
64
69
  try {
65
70
  writeJson(paths.scenariosPath, bundle);
66
- } catch (err: any) {
67
- throw new Error(`시나리오 파일 저장 실패: ${err.message}`);
71
+ } catch (err: unknown) {
72
+ throw new Error(`시나리오 파일 저장 실패: ${err instanceof Error ? err.message : String(err)}`);
68
73
  }
69
74
 
70
75
  return { scenariosPath: paths.scenariosPath, count: bundle.scenarios.length };
@@ -0,0 +1,60 @@
1
+ /**
2
+ * Lightweight type interfaces for ts-morph (lazily imported).
3
+ * These mirror the subset of ts-morph API used by ATE,
4
+ * avoiding a hard compile-time dependency on ts-morph.
5
+ */
6
+
7
+ /** Minimal ts-morph Node interface */
8
+ export interface Node {
9
+ getKind(): number;
10
+ getText(): string;
11
+ getDescendantsOfKind(kind: number): Node[];
12
+ }
13
+
14
+ /** ts-morph JsxAttribute */
15
+ export interface JsxAttribute extends Node {
16
+ getNameNode?(): { getText(): string };
17
+ getName?(): string;
18
+ getInitializer?(): (Node & { getLiteralValue?(): string }) | undefined;
19
+ }
20
+
21
+ /** ts-morph CallExpression */
22
+ export interface CallExpression extends Node {
23
+ getExpression(): Node;
24
+ getArguments(): (Node & { getLiteralValue?(): string })[];
25
+ }
26
+
27
+ /** ts-morph SourceFile */
28
+ export interface SourceFile extends Node {
29
+ getFilePath(): string;
30
+ getExportedDeclarations(): ReadonlyMap<string, Node[]>;
31
+ getDescendantsOfKind(kind: number): Node[];
32
+ getImportDeclarations(): ImportDeclaration[];
33
+ getExportDeclarations(): ExportDeclaration[];
34
+ }
35
+
36
+ /** ts-morph ImportDeclaration */
37
+ export interface ImportDeclaration extends Node {
38
+ getModuleSpecifierValue(): string;
39
+ }
40
+
41
+ /** ts-morph ExportDeclaration */
42
+ export interface ExportDeclaration extends Node {
43
+ getModuleSpecifier(): (Node & { getLiteralText(): string }) | undefined;
44
+ }
45
+
46
+ /** ts-morph Project */
47
+ export interface Project {
48
+ addSourceFileAtPath(filePath: string): SourceFile;
49
+ addSourceFilesAtPaths(globs: string[]): SourceFile[];
50
+ getSourceFile(filePath: string): SourceFile | undefined;
51
+ getSourceFiles(): SourceFile[];
52
+ }
53
+
54
+ /** SyntaxKind enum-like object used via dynamic import */
55
+ export interface SyntaxKindEnum {
56
+ StringLiteral: number;
57
+ JsxAttribute: number;
58
+ CallExpression: number;
59
+ [key: string]: string | number;
60
+ }
package/src/types.ts CHANGED
@@ -36,7 +36,7 @@ export interface InteractionGraph {
36
36
  }
37
37
 
38
38
  export type InteractionNode =
39
- | { kind: "route"; id: string; file: string; path: string }
39
+ | { kind: "route"; id: string; file: string; path: string; methods?: string[] }
40
40
  | { kind: "modal"; id: string; file: string; name: string }
41
41
  | { kind: "action"; id: string; file: string; name: string };
42
42