JA Development Guides Debugging - aku11i/phantom GitHub Wiki

デバッグガイド

English | 日本語

このガイドでは、開発中のPhantomのトラブルシューティングのためのデバッグテクニックとツールについて説明します。

デバッグツール

Node.jsデバッガー

Node.jsには、Chrome DevToolsとVS Codeで動作する組み込みデバッガーが含まれています。

コマンドラインデバッグ

# インスペクター付きで開始
node --inspect dist/phantom.js create test

# インスペクター付きで開始し、最初の行でブレーク
node --inspect-brk dist/phantom.js create test

# 特定のポート
node --inspect=9229 dist/phantom.js list

Chrome DevTools

  1. --inspectでNodeを開始
  2. Chromeを開いてchrome://inspectに移動
  3. Remote Targetの下の「inspect」をクリック
  4. DevToolsを使用してデバッグ

VS Codeデバッグ

起動設定

ソース: 推奨される.vscode/launch.json

{
  "version": "0.2.0",
  "configurations": [
    {
      "type": "node",
      "request": "launch",
      "name": "Debug Phantom CLI",
      "skipFiles": ["<node_internals>/**"],
      "program": "${workspaceFolder}/dist/phantom.js",
      "args": ["create", "test-phantom"],
      "outFiles": ["${workspaceFolder}/dist/**/*.js"],
      "sourceMaps": true,
      "console": "integratedTerminal"
    },
    {
      "type": "node",
      "request": "launch",
      "name": "Debug Current Test",
      "skipFiles": ["<node_internals>/**"],
      "program": "${file}",
      "args": ["--test"],
      "console": "integratedTerminal"
    },
    {
      "type": "node",
      "request": "attach",
      "name": "Attach to Process",
      "port": 9229,
      "skipFiles": ["<node_internals>/**"],
      "sourceMaps": true
    }
  ]
}

TypeScriptソースのデバッグ

Phantomはesbuildでビルドされているため、ソースマップによりTypeScriptのデバッグが可能です:

# ソースマップ付きでビルド(デフォルト)
pnpm build

# TypeScriptを直接デバッグ
node --enable-source-maps --inspect dist/phantom.js

一般的なデバッグシナリオ

1. コマンドが動作しない

// ハンドラーにデバッグロギングを追加
export async function createHandler(args: string[]): Promise<void> {
  console.log("createHandler called with:", args);
  
  const [name, branch] = args;
  console.log("Parsed:", { name, branch });
  
  // 既存のコード...
}

2. Gitコマンドの失敗

Git操作をデバッグ:

// src/core/git/executor.ts内
export class GitExecutor {
  execute(args: string[]): Result<string> {
    console.log("Git command:", "git", args.join(" "));
    
    const { status, stdout, stderr } = spawnSync("git", args);
    
    console.log("Git exit code:", status);
    console.log("Git stdout:", stdout);
    console.log("Git stderr:", stderr);
    
    // 既存のコード...
  }
}

3. パス解決の問題

// パス構築をデバッグ
import { getWorktreePath } from "../paths.js";

const path = getWorktreePath(repoName, phantomName);
console.log("Resolved path:", {
  repoName,
  phantomName,
  resultPath: path,
  exists: existsSync(path)
});

4. Result型のデバッグ

// Result型をログするヘルパー
function debugResult<T>(result: Result<T>, label: string): void {
  if (result.ok) {
    console.log(`${label} - Success:`, result.value);
  } else {
    console.log(`${label} - Error:`, result.error.message);
    console.error(result.error.stack);
  }
}

// 使用方法
const result = await createWorktree(name);
debugResult(result, "createWorktree");

テストのデバッグ

デバッグ付きで単一テストを実行

# 特定のテストファイルを実行
node --inspect --test src/core/worktree/create.test.ts

# パターンに一致するテストを実行
node --inspect --test --test-name-pattern="validation"

モックの動作をデバッグ

import { mock } from "node:test";

// モック呼び出しをログ
const mockExecute = mock.fn((args) => {
  console.log("Mock called with:", args);
  return Result.ok("mocked response");
});

// テスト後
console.log("Mock call count:", mockExecute.mock.calls.length);
console.log("All calls:", mockExecute.mock.calls);

テストのタイムアウト問題

import { test } from "node:test";

// デバッグのためにタイムアウトを増やす
test("slow operation", { timeout: 30000 }, async () => {
  // ブレークポイントを設定し、タイムアウトなしでデバッグ
});

デバッグテクニック

1. バイナリサーチデバッグ

問題がどこで発生するか不明な場合:

console.log("Point A reached");
// コードの半分
console.log("Point B reached");
// もう半分
console.log("Point C reached");

2. 状態の検査

// デバッグヘルパーを作成
function inspectState(label: string, state: any): void {
  console.log(`\n=== ${label} ===`);
  console.log(JSON.stringify(state, null, 2));
  console.log("=================\n");
}

// コード全体で使用
inspectState("Before validation", { name, branch });
inspectState("After Git operation", { result, worktrees });

3. エラースタックトレース

// エラー情報を強化
try {
  await riskyOperation();
} catch (error) {
  console.error("Operation failed");
  console.error("Error:", error.message);
  console.error("Stack:", error.stack);
  console.error("Context:", { name, branch, path });
  throw error;
}

4. 条件付きデバッグ

// 環境ベースのデバッグ
const DEBUG = process.env.DEBUG_PHANTOM === "true";

if (DEBUG) {
  console.log("Debug info:", data);
}

// またはデバッグ関数を使用
function debug(...args: any[]): void {
  if (process.env.DEBUG_PHANTOM) {
    console.log("[DEBUG]", ...args);
  }
}

Git Worktreeのデバッグ

Git状態の検査

# worktree状態を確認
git worktree list --porcelain

# Git内部を確認
ls -la .git/worktrees/

# worktree設定を表示
cat .git/worktrees/phantom-name/gitdir

一般的なGitの問題

// Git worktreeの問題をデバッグ
async function debugWorktreeState(name: string): Promise<void> {
  const git = new GitExecutor();
  
  // すべてのworktreeをリスト
  const listResult = git.execute(["worktree", "list", "--porcelain"]);
  console.log("All worktrees:", listResult);
  
  // 特定のworktreeを確認
  const worktreePath = getWorktreePath(repoName, name);
  console.log("Checking path:", worktreePath);
  console.log("Path exists:", existsSync(worktreePath));
  console.log("Is directory:", existsSync(worktreePath) && 
    statSync(worktreePath).isDirectory());
  
  // Gitディレクトリを確認
  const gitDir = join(worktreePath, ".git");
  console.log("Git dir:", gitDir);
  console.log("Git file contents:", 
    existsSync(gitDir) ? readFileSync(gitDir, "utf-8") : "Not found");
}

パフォーマンスデバッグ

操作のタイミング

// シンプルなタイミング
console.time("createWorktree");
const result = await createWorktree(name);
console.timeEnd("createWorktree");

// 詳細なタイミング
const start = performance.now();
const result = await operation();
const duration = performance.now() - start;
console.log(`操作に${duration.toFixed(2)}msかかりました`);

メモリプロファイリング

// メモリ使用量の追跡
function logMemoryUsage(label: string): void {
  const usage = process.memoryUsage();
  console.log(`メモリ (${label}):`);
  console.log(`  RSS: ${(usage.rss / 1024 / 1024).toFixed(2)} MB`);
  console.log(`  ヒープ使用: ${(usage.heapUsed / 1024 / 1024).toFixed(2)} MB`);
  console.log(`  ヒープ合計: ${(usage.heapTotal / 1024 / 1024).toFixed(2)} MB`);
}

logMemoryUsage("操作前");
await heavyOperation();
logMemoryUsage("操作後");

CPUプロファイリング

# プロファイリング付きで開始
node --prof dist/phantom.js create test

# ログを処理
node --prof-process isolate-*.log > profile.txt

# profile.txtでボトルネックを分析

本番環境の問題のデバッグ

詳細ロギング

// 詳細モードを追加
const VERBOSE = process.env.PHANTOM_VERBOSE === "true";

function verbose(message: string, data?: any): void {
  if (VERBOSE) {
    console.error(`[VERBOSE] ${message}`, data || "");
  }
}

// 全体で使用
verbose("Creating worktree", { name, branch });
verbose("Git command", { cmd: args.join(" ") });

エラーコンテキスト

// 拡張エラークラス
class PhantomError extends Error {
  constructor(
    message: string,
    public code: string,
    public context: Record<string, any>
  ) {
    super(message);
    this.name = "PhantomError";
  }
}

// 使用方法
throw new PhantomError(
  "Failed to create worktree",
  "WORKTREE_CREATE_FAILED",
  { name, branch, gitError: stderr }
);

デバッグユーティリティ

デバッグモジュール

デバッグユーティリティを作成:

// src/core/debug.ts
const DEBUG = process.env.DEBUG?.includes("phantom");

export function debug(namespace: string) {
  return (...args: any[]) => {
    if (DEBUG || process.env.DEBUG?.includes(namespace)) {
      console.error(`[${namespace}]`, ...args);
    }
  };
}

// 使用方法
import { debug } from "../debug.js";
const log = debug("phantom:git");

log("Executing command", args);

検査ヘルパー

// デバッグ用のきれいな出力
export function inspect(obj: any, label?: string): void {
  if (label) console.log(`\n${label}:`);
  console.dir(obj, {
    depth: null,
    colors: true,
    compact: false
  });
}

// ブレークポイントヘルパー
export function breakpoint(condition: boolean = true): void {
  if (condition) {
    debugger;
  }
}

一般的なデバッグパターン

1. 実行フローのトレース

class ExecutionTracer {
  private stack: string[] = [];
  
  enter(name: string): void {
    this.stack.push(name);
    console.log("→".repeat(this.stack.length), name);
  }
  
  exit(name: string): void {
    console.log("←".repeat(this.stack.length), name);
    this.stack.pop();
  }
}

const tracer = new ExecutionTracer();
tracer.enter("createWorktree");
// ... コード ...
tracer.exit("createWorktree");

2. アサーションヘルパー

function assertDefined<T>(
  value: T | undefined,
  message: string
): asserts value is T {
  if (value === undefined) {
    console.error("Assertion failed:", message);
    console.trace();
    throw new Error(message);
  }
}

// 使用方法
assertDefined(worktreeName, "Worktree name is required");

3. 状態スナップショット

// 異なるポイントで状態をキャプチャ
const stateHistory: any[] = [];

function captureState(label: string, state: any): void {
  stateHistory.push({
    timestamp: Date.now(),
    label,
    state: JSON.parse(JSON.stringify(state))
  });
}

function dumpStateHistory(): void {
  console.log("State History:");
  stateHistory.forEach(({ timestamp, label, state }) => {
    console.log(`\n[${new Date(timestamp).toISOString()}] ${label}:`);
    console.log(JSON.stringify(state, null, 2));
  });
}

ヒントとコツ

1. 説明的なログを使用

// 悪い
console.log(result);
console.log(name);

// 良い
console.log("Worktree creation result:", result);
console.log("Processing phantom name:", name);

2. デバッグコードを保護

// デバッグコードが本番環境に到達しないようにする
if (process.env.NODE_ENV !== "production") {
  console.log("Debug:", sensitiveData);
}

3. デバッグコードをクリーンアップ

# 残っているデバッグステートメントを見つける
grep -r "console\." src/
grep -r "debugger" src/

4. ソースマップを使用

# 開発では常にソースマップを有効にする
node --enable-source-maps dist/phantom.js

# デバッグ用にインラインソースマップでビルド
# (一時的にbuild.tsを変更)
sourcemap: "inline"

まとめ

Phantomでの効果的なデバッグには以下が含まれます:

  1. 適切なツール - VS Codeデバッガー、Chrome DevTools
  2. 戦略的なロギング - 意味のあるデバッグ出力
  3. 状態の検査 - データフローの理解
  4. 分離 - 一度に1つのことをデバッグ
  5. クリーンアップ - コミット前にデバッグコードを削除

覚えておいてください:良いデバッグは系統的で忍耐強いものです。解決策に飛びつく前に、問題を理解するために時間をかけてください。

⚠️ **GitHub.com Fallback** ⚠️