From f26793ca545a2fab3bb89414347f7cb519a5aff2 Mon Sep 17 00:00:00 2001 From: Vincent Koc Date: Mon, 13 Jul 2026 10:42:29 +0800 Subject: [PATCH] refactor(qa-lab): trim process metric parser exports (#105891) --- .../qa-lab/src/process-tree-cpu.test.ts | 164 ++++++++---------- extensions/qa-lab/src/process-tree-cpu.ts | 10 +- .../src/process-tree-cpu.windows.test.ts | 69 +++++++- scripts/deadcode-exports.baseline.mjs | 5 - 4 files changed, 142 insertions(+), 106 deletions(-) diff --git a/extensions/qa-lab/src/process-tree-cpu.test.ts b/extensions/qa-lab/src/process-tree-cpu.test.ts index e6db946c2a16..569d52f83771 100644 --- a/extensions/qa-lab/src/process-tree-cpu.test.ts +++ b/extensions/qa-lab/src/process-tree-cpu.test.ts @@ -1,107 +1,83 @@ -// Qa Lab tests cover process tree cpu plugin behavior. -import { describe, expect, it } from "vitest"; -import { - parsePsCpuTimeMs, - parsePsRssBytes, - parseWindowsProcessCpuTimeMs, - parseWindowsProcessTreeSnapshot, - parseWindowsWorkingSetBytes, -} from "./process-tree-cpu.js"; +// Qa Lab tests cover POSIX process tree metric sampling. +import { afterEach, describe, expect, it, vi } from "vitest"; -describe("process tree CPU helpers", () => { - it("parses ps CPU time strings", () => { - expect(parsePsCpuTimeMs("00:01")).toBe(1_000); - expect(parsePsCpuTimeMs("00:00.12")).toBe(120); - expect(parsePsCpuTimeMs("01:02")).toBe(62_000); - expect(parsePsCpuTimeMs("01:02:03.45")).toBe(3_723_450); - expect(parsePsCpuTimeMs("1-02:03:04.5")).toBe(93_784_500); +const spawnSyncMock = vi.hoisted(() => vi.fn()); + +vi.mock("node:child_process", async () => { + const actual = await vi.importActual("node:child_process"); + return { + ...actual, + spawnSync: spawnSyncMock, + }; +}); + +import { readProcessTreeCpuMs, readProcessTreeRssBytes } from "./process-tree-cpu.js"; + +afterEach(() => { + vi.restoreAllMocks(); + spawnSyncMock.mockReset(); +}); + +function usePsOutput(stdout: string): void { + vi.spyOn(process, "platform", "get").mockReturnValue("linux"); + spawnSyncMock.mockReturnValue({ status: 0, stdout }); +} + +describe("POSIX process tree metrics", () => { + it("parses ps CPU time formats", () => { + usePsOutput( + [ + "100 0 00:01", + "101 0 00:00.12", + "102 0 01:02", + "103 0 01:02:03.45", + "104 0 1-02:03:04.5", + ].join("\n"), + ); + + expect(readProcessTreeCpuMs(100)).toBe(1_000); + expect(readProcessTreeCpuMs(101)).toBe(120); + expect(readProcessTreeCpuMs(102)).toBe(62_000); + expect(readProcessTreeCpuMs(103)).toBe(3_723_450); + expect(readProcessTreeCpuMs(104)).toBe(93_784_500); + expect(spawnSyncMock.mock.calls[0]?.slice(0, 2)).toEqual(["ps", ["-eo", "pid=,ppid=,time="]]); }); it("rejects malformed ps CPU time strings", () => { - expect(parsePsCpuTimeMs("")).toBeNull(); - expect(parsePsCpuTimeMs("nope")).toBeNull(); - expect(parsePsCpuTimeMs("1::02")).toBeNull(); - expect(parsePsCpuTimeMs("1-02:03")).toBeNull(); - expect(parsePsCpuTimeMs("01:60")).toBeNull(); - expect(parsePsCpuTimeMs("01:02:60")).toBeNull(); - expect(parsePsCpuTimeMs("1:2:3:4")).toBeNull(); + usePsOutput( + [ + "101 0 nope", + "102 0 1::02", + "103 0 1-02:03", + "104 0 01:60", + "105 0 01:02:60", + "106 0 1:2:3:4", + ].join("\n"), + ); + + expect(readProcessTreeCpuMs(100)).toBeNull(); + expect(readProcessTreeCpuMs(101)).toBeNull(); + expect(readProcessTreeCpuMs(102)).toBeNull(); + expect(readProcessTreeCpuMs(103)).toBeNull(); + expect(readProcessTreeCpuMs(104)).toBeNull(); + expect(readProcessTreeCpuMs(105)).toBeNull(); + expect(readProcessTreeCpuMs(106)).toBeNull(); }); it("parses ps RSS KiB values as bytes", () => { - expect(parsePsRssBytes("1024")).toBe(1_048_576); - expect(parsePsRssBytes("1.5")).toBe(1_536); + usePsOutput(["100 0 1024", "101 0 1.5"].join("\n")); + + expect(readProcessTreeRssBytes(100)).toBe(1_048_576); + expect(readProcessTreeRssBytes(101)).toBe(1_536); + expect(spawnSyncMock.mock.calls[0]?.slice(0, 2)).toEqual(["ps", ["-eo", "pid=,ppid=,rss="]]); }); it("rejects malformed ps RSS values", () => { - expect(parsePsRssBytes("")).toBeNull(); - expect(parsePsRssBytes("nope")).toBeNull(); - expect(parsePsRssBytes("-1")).toBeNull(); - expect(parsePsRssBytes("0x10")).toBeNull(); - }); + usePsOutput(["101 0 nope", "102 0 -1", "103 0 0x10"].join("\n")); - it("parses Windows process CPU and RSS counters", () => { - expect( - parseWindowsProcessCpuTimeMs({ - kernelModeTime: "20000", - userModeTime: 30_000, - }), - ).toBe(5); - expect(parseWindowsWorkingSetBytes("1048576")).toBe(1_048_576); - }); - - it("rejects non-decimal Windows process counters", () => { - expect( - parseWindowsProcessCpuTimeMs({ - kernelModeTime: "0x10", - userModeTime: "30000", - }), - ).toBeNull(); - expect(parseWindowsWorkingSetBytes("0x1000")).toBeNull(); - }); - - it("builds Windows process tree snapshots from PowerShell JSON", () => { - const snapshot = parseWindowsProcessTreeSnapshot( - JSON.stringify([ - { - ProcessId: 100, - ParentProcessId: 50, - KernelModeTime: "10000", - UserModeTime: "20000", - WorkingSetSize: "1000", - }, - { - ProcessId: 101, - ParentProcessId: 100, - KernelModeTime: "30000", - UserModeTime: "40000", - WorkingSetSize: "2000", - }, - ]), - ); - - expect(snapshot?.childrenByParent.get(50)).toEqual([100]); - expect(snapshot?.childrenByParent.get(100)).toEqual([101]); - expect(snapshot?.cpuByPid.get(100)).toBe(3); - expect(snapshot?.cpuByPid.get(101)).toBe(7); - expect(snapshot?.rssByPid.get(100)).toBe(1000); - expect(snapshot?.rssByPid.get(101)).toBe(2000); - }); - - it("skips Windows process entries with non-decimal process ids", () => { - const snapshot = parseWindowsProcessTreeSnapshot( - JSON.stringify([ - { - ProcessId: "0x64", - ParentProcessId: 50, - KernelModeTime: "10000", - UserModeTime: "20000", - WorkingSetSize: "1000", - }, - ]), - ); - - expect(snapshot?.childrenByParent.size).toBe(0); - expect(snapshot?.cpuByPid.size).toBe(0); - expect(snapshot?.rssByPid.size).toBe(0); + expect(readProcessTreeRssBytes(100)).toBeNull(); + expect(readProcessTreeRssBytes(101)).toBeNull(); + expect(readProcessTreeRssBytes(102)).toBeNull(); + expect(readProcessTreeRssBytes(103)).toBeNull(); }); }); diff --git a/extensions/qa-lab/src/process-tree-cpu.ts b/extensions/qa-lab/src/process-tree-cpu.ts index 9066cead3938..279a53ecf4c5 100644 --- a/extensions/qa-lab/src/process-tree-cpu.ts +++ b/extensions/qa-lab/src/process-tree-cpu.ts @@ -37,7 +37,7 @@ function parseNonNegativeNumber(value: unknown): number | null { return parsed; } -export function parsePsCpuTimeMs(raw: string): number | null { +function parsePsCpuTimeMs(raw: string): number | null { const match = raw.trim().match(/^(?:(\d+)-)?(\d+):(\d{2}(?:\.\d+)?)(?::(\d{2}(?:\.\d+)?))?$/u); if (!match) { return null; @@ -69,7 +69,7 @@ export function parsePsCpuTimeMs(raw: string): number | null { return Math.round((first * 60 + second) * 1000); } -export function parsePsRssBytes(raw: string): number | null { +function parsePsRssBytes(raw: string): number | null { const trimmed = raw.trim(); if (!trimmed) { return null; @@ -81,7 +81,7 @@ export function parsePsRssBytes(raw: string): number | null { return Math.round(rssKiB * 1024); } -export function parseWindowsProcessCpuTimeMs(params: { +function parseWindowsProcessCpuTimeMs(params: { kernelModeTime: unknown; userModeTime: unknown; }): number | null { @@ -93,12 +93,12 @@ export function parseWindowsProcessCpuTimeMs(params: { return Math.round((kernelModeTime + userModeTime) / 10_000); } -export function parseWindowsWorkingSetBytes(raw: unknown): number | null { +function parseWindowsWorkingSetBytes(raw: unknown): number | null { const parsed = parseNonNegativeNumber(raw); return parsed === null ? null : Math.round(parsed); } -export function parseWindowsProcessTreeSnapshot(raw: string): ProcessTreeSnapshot | null { +function parseWindowsProcessTreeSnapshot(raw: string): ProcessTreeSnapshot | null { let parsed: unknown; try { parsed = JSON.parse(raw); diff --git a/extensions/qa-lab/src/process-tree-cpu.windows.test.ts b/extensions/qa-lab/src/process-tree-cpu.windows.test.ts index ea758d0d922c..7073d47eaa53 100644 --- a/extensions/qa-lab/src/process-tree-cpu.windows.test.ts +++ b/extensions/qa-lab/src/process-tree-cpu.windows.test.ts @@ -12,7 +12,7 @@ vi.mock("node:child_process", async () => { }; }); -import { readProcessTreeCpuMs } from "./process-tree-cpu.js"; +import { readProcessTreeCpuMs, readProcessTreeRssBytes } from "./process-tree-cpu.js"; afterEach(() => { vi.restoreAllMocks(); @@ -21,7 +21,24 @@ afterEach(() => { }); describe("readProcessTreeCpuMs on Windows", () => { - it("uses the trusted Windows PowerShell path", () => { + it("parses single-process Windows CPU and RSS counters", () => { + vi.spyOn(process, "platform", "get").mockReturnValue("win32"); + spawnSyncMock.mockReturnValue({ + status: 0, + stdout: JSON.stringify({ + ProcessId: 100, + ParentProcessId: 50, + KernelModeTime: "20000", + UserModeTime: 30_000, + WorkingSetSize: "1048576", + }), + }); + + expect(readProcessTreeCpuMs(100)).toBe(5); + expect(readProcessTreeRssBytes(100)).toBe(1_048_576); + }); + + it("parses process tree CPU and RSS metrics through the trusted PowerShell path", () => { vi.spyOn(process, "platform", "get").mockReturnValue("win32"); vi.stubEnv("SystemRoot", "D:\\Windows"); spawnSyncMock.mockReturnValue({ @@ -45,8 +62,56 @@ describe("readProcessTreeCpuMs on Windows", () => { }); expect(readProcessTreeCpuMs(100)).toBe(10); + expect(readProcessTreeCpuMs(101)).toBe(7); + expect(readProcessTreeRssBytes(100)).toBe(3_000); + expect(readProcessTreeRssBytes(101)).toBe(2_000); expect(spawnSyncMock.mock.calls[0]?.[0]).toBe( path.win32.join("D:\\Windows", "System32", "WindowsPowerShell", "v1.0", "powershell.exe"), ); }); + + it("rejects non-decimal Windows process counters", () => { + vi.spyOn(process, "platform", "get").mockReturnValue("win32"); + spawnSyncMock.mockReturnValue({ + status: 0, + stdout: JSON.stringify({ + ProcessId: 100, + ParentProcessId: 50, + KernelModeTime: "0x10", + UserModeTime: "30000", + WorkingSetSize: "0x1000", + }), + }); + + expect(readProcessTreeCpuMs(100)).toBeNull(); + expect(readProcessTreeRssBytes(100)).toBeNull(); + }); + + it("skips Windows process entries with non-decimal process ids", () => { + vi.spyOn(process, "platform", "get").mockReturnValue("win32"); + spawnSyncMock.mockReturnValue({ + status: 0, + stdout: JSON.stringify({ + ProcessId: "0x64", + ParentProcessId: 50, + KernelModeTime: "10000", + UserModeTime: "20000", + WorkingSetSize: "1000", + }), + }); + + expect(readProcessTreeCpuMs(100)).toBeNull(); + expect(readProcessTreeRssBytes(100)).toBeNull(); + }); + + it("rejects malformed Windows process snapshots", () => { + vi.spyOn(process, "platform", "get").mockReturnValue("win32"); + spawnSyncMock.mockReturnValue({ + status: 0, + stdout: "not json", + }); + + expect(readProcessTreeCpuMs(100)).toBeNull(); + expect(readProcessTreeRssBytes(100)).toBeNull(); + }); }); diff --git a/scripts/deadcode-exports.baseline.mjs b/scripts/deadcode-exports.baseline.mjs index 2075ff588792..9713defd622c 100644 --- a/scripts/deadcode-exports.baseline.mjs +++ b/scripts/deadcode-exports.baseline.mjs @@ -1301,11 +1301,6 @@ export const KNIP_UNUSED_EXPORT_BASELINE = [ "extensions/qa-lab/src/model-selection.runtime.ts: resolveQaPreferredLiveModel", "extensions/qa-lab/src/multipass.runtime.ts: createQaMultipassPlan", "extensions/qa-lab/src/multipass.runtime.ts: renderQaMultipassGuestScript", - "extensions/qa-lab/src/process-tree-cpu.ts: parsePsCpuTimeMs", - "extensions/qa-lab/src/process-tree-cpu.ts: parsePsRssBytes", - "extensions/qa-lab/src/process-tree-cpu.ts: parseWindowsProcessCpuTimeMs", - "extensions/qa-lab/src/process-tree-cpu.ts: parseWindowsProcessTreeSnapshot", - "extensions/qa-lab/src/process-tree-cpu.ts: parseWindowsWorkingSetBytes", "extensions/qa-lab/src/providers/mock-openai/server.ts: MockOpenAiProviderVariant", "extensions/qa-lab/src/providers/mock-openai/server.ts: resolveProviderVariant", "extensions/qa-lab/src/qa-agent-workspace.ts: __testing",