From ce431273ebda03611fc8402489ae54778626c654 Mon Sep 17 00:00:00 2001 From: Thanh Trinh Date: Tue, 4 Aug 2026 15:53:26 -0500 Subject: [PATCH 1/5] feat: install dependencies from selected task Co-authored-by: Codex --- .../api/install-deps/__tests__/route.test.ts | 57 +++++++++++++++++++ src/app/api/install-deps/route.ts | 35 ++++-------- 2 files changed, 68 insertions(+), 24 deletions(-) create mode 100644 src/app/api/install-deps/__tests__/route.test.ts diff --git a/src/app/api/install-deps/__tests__/route.test.ts b/src/app/api/install-deps/__tests__/route.test.ts new file mode 100644 index 0000000..329fc50 --- /dev/null +++ b/src/app/api/install-deps/__tests__/route.test.ts @@ -0,0 +1,57 @@ +import { jest, describe, it, expect, beforeEach } from '@jest/globals'; +import { NextRequest } from 'next/server'; + +const mockAccess = jest.fn<(path: string) => Promise>(); +const mockRm = + jest.fn<(path: string, options: { recursive: boolean; force: boolean }) => Promise>(); +const mockExecAsync = jest.fn<() => Promise<{ stdout: string; stderr: string }>>(); +const mockFindContractDeploymentsRoot = jest.fn<() => string>(); + +jest.unstable_mockModule('fs', () => ({ + promises: { access: mockAccess, rm: mockRm }, +})); + +jest.unstable_mockModule('util', () => ({ + promisify: () => mockExecAsync, +})); + +jest.unstable_mockModule('@/lib/deployments', () => ({ + findContractDeploymentsRoot: mockFindContractDeploymentsRoot, +})); + +const { POST } = await import('../route'); + +function createRequest(body: object): NextRequest { + return new NextRequest('http://localhost/api/install-deps', { + method: 'POST', + body: JSON.stringify(body), + headers: { 'content-type': 'application/json' }, + }); +} + +describe('POST /api/install-deps', () => { + beforeEach(() => { + jest.clearAllMocks(); + jest.spyOn(console, 'log').mockImplementation(() => {}); + mockFindContractDeploymentsRoot.mockReturnValue('/repo'); + mockAccess.mockResolvedValue(undefined); + mockRm.mockResolvedValue(undefined); + mockExecAsync.mockResolvedValue({ stdout: 'installed', stderr: '' }); + }); + + it('purges shared libs and installs with the selected task Makefile', async () => { + const response = await POST( + createRequest({ network: 'zeronet', upgradeId: '2026-07-10-transfer-owner' }) + ); + + expect(response.status).toBe(200); + expect(mockRm).toHaveBeenCalledWith('/repo/active/evm/lib', { + recursive: true, + force: true, + }); + expect(mockExecAsync).toHaveBeenCalledWith( + 'make -f tasks/2026-07-10-transfer-owner/Makefile deps', + expect.objectContaining({ cwd: '/repo/active/evm' }) + ); + }); +}); diff --git a/src/app/api/install-deps/route.ts b/src/app/api/install-deps/route.ts index 3a327a8..3650500 100644 --- a/src/app/api/install-deps/route.ts +++ b/src/app/api/install-deps/route.ts @@ -21,7 +21,7 @@ const pathExists = async (targetPath: string) => { export async function POST(req: NextRequest) { try { const json = await req.json(); - const { network, upgradeId, forceInstall } = json; + const { network, upgradeId } = json; if (!network || !upgradeId) { return NextResponse.json( @@ -31,8 +31,6 @@ export async function POST(req: NextRequest) { } const actualNetwork = network.toLowerCase(); - const shouldForceInstall = Boolean(forceInstall); - const safePathPattern = /^[a-zA-Z0-9_-]+$/; if (!safePathPattern.test(actualNetwork) || !safePathPattern.test(upgradeId)) { return NextResponse.json( @@ -58,30 +56,17 @@ export async function POST(req: NextRequest) { const libPath = path.join(resolvedUpgradePath, 'lib'); const resolvedTaskPath = assertWithinDir(taskPath, contractDeploymentsPath); + const taskMakefilePath = assertWithinDir( + path.join(resolvedTaskPath, 'Makefile'), + resolvedTaskPath + ); - const taskPathExists = await pathExists(resolvedTaskPath); - if (!taskPathExists) { - return NextResponse.json( - { error: `Task folder not found: ${path.relative(contractDeploymentsPath, taskPath)}` }, - { status: 404 } - ); - } - - const libExistsBeforeInstall = await pathExists(libPath); - - if (!shouldForceInstall && libExistsBeforeInstall) { - console.log(`Deps already installed for ${actualNetwork}/${upgradeId}; skipping.`); + if (!(await pathExists(taskMakefilePath))) { return NextResponse.json( { - success: true, - message: `Dependencies already installed for ${actualNetwork}/${upgradeId}`, - libExists: true, - installed: false, - depsInstalled: false, - stdout: '', - stderr: '', + error: `Task Makefile not found: ${path.relative(contractDeploymentsPath, taskMakefilePath)}`, }, - { status: 200 } + { status: 404 } ); } @@ -89,7 +74,9 @@ export async function POST(req: NextRequest) { `Installing dependencies for ${actualNetwork}/${upgradeId} (cwd: ${resolvedUpgradePath})` ); - const { stdout, stderr } = await execAsync('make deps', { + await fs.rm(libPath, { recursive: true, force: true }); + + const { stdout, stderr } = await execAsync(`make -f tasks/${upgradeId}/Makefile deps`, { cwd: resolvedUpgradePath, timeout: INSTALL_DEPS_TIMEOUT_MS, env: process.env, From d8e9988d8fba7cb442da35309c221c0c1eef00a5 Mon Sep 17 00:00:00 2001 From: Thanh Trinh Date: Tue, 4 Aug 2026 15:55:27 -0500 Subject: [PATCH 2/5] refactor: preserve dependency cache behavior Co-authored-by: Codex --- .../api/install-deps/__tests__/route.test.ts | 28 ++++++++++++------- src/app/api/install-deps/route.ts | 23 +++++++++++++-- 2 files changed, 38 insertions(+), 13 deletions(-) diff --git a/src/app/api/install-deps/__tests__/route.test.ts b/src/app/api/install-deps/__tests__/route.test.ts index 329fc50..955a5a9 100644 --- a/src/app/api/install-deps/__tests__/route.test.ts +++ b/src/app/api/install-deps/__tests__/route.test.ts @@ -2,13 +2,11 @@ import { jest, describe, it, expect, beforeEach } from '@jest/globals'; import { NextRequest } from 'next/server'; const mockAccess = jest.fn<(path: string) => Promise>(); -const mockRm = - jest.fn<(path: string, options: { recursive: boolean; force: boolean }) => Promise>(); const mockExecAsync = jest.fn<() => Promise<{ stdout: string; stderr: string }>>(); const mockFindContractDeploymentsRoot = jest.fn<() => string>(); jest.unstable_mockModule('fs', () => ({ - promises: { access: mockAccess, rm: mockRm }, + promises: { access: mockAccess }, })); jest.unstable_mockModule('util', () => ({ @@ -34,24 +32,34 @@ describe('POST /api/install-deps', () => { jest.clearAllMocks(); jest.spyOn(console, 'log').mockImplementation(() => {}); mockFindContractDeploymentsRoot.mockReturnValue('/repo'); - mockAccess.mockResolvedValue(undefined); - mockRm.mockResolvedValue(undefined); mockExecAsync.mockResolvedValue({ stdout: 'installed', stderr: '' }); }); - it('purges shared libs and installs with the selected task Makefile', async () => { + it('installs missing shared libs with the selected task Makefile', async () => { + mockAccess + .mockResolvedValueOnce(undefined) + .mockRejectedValueOnce(new Error('missing')) + .mockResolvedValueOnce(undefined); + const response = await POST( createRequest({ network: 'zeronet', upgradeId: '2026-07-10-transfer-owner' }) ); expect(response.status).toBe(200); - expect(mockRm).toHaveBeenCalledWith('/repo/active/evm/lib', { - recursive: true, - force: true, - }); expect(mockExecAsync).toHaveBeenCalledWith( 'make -f tasks/2026-07-10-transfer-owner/Makefile deps', expect.objectContaining({ cwd: '/repo/active/evm' }) ); }); + + it('keeps existing shared libs', async () => { + mockAccess.mockResolvedValue(undefined); + + const response = await POST( + createRequest({ network: 'zeronet', upgradeId: '2026-07-10-transfer-owner' }) + ); + + expect(response.status).toBe(200); + expect(mockExecAsync).not.toHaveBeenCalled(); + }); }); diff --git a/src/app/api/install-deps/route.ts b/src/app/api/install-deps/route.ts index 3650500..52d40ef 100644 --- a/src/app/api/install-deps/route.ts +++ b/src/app/api/install-deps/route.ts @@ -21,7 +21,7 @@ const pathExists = async (targetPath: string) => { export async function POST(req: NextRequest) { try { const json = await req.json(); - const { network, upgradeId } = json; + const { network, upgradeId, forceInstall } = json; if (!network || !upgradeId) { return NextResponse.json( @@ -31,6 +31,7 @@ export async function POST(req: NextRequest) { } const actualNetwork = network.toLowerCase(); + const shouldForceInstall = Boolean(forceInstall); const safePathPattern = /^[a-zA-Z0-9_-]+$/; if (!safePathPattern.test(actualNetwork) || !safePathPattern.test(upgradeId)) { return NextResponse.json( @@ -70,12 +71,28 @@ export async function POST(req: NextRequest) { ); } + const libExistsBeforeInstall = await pathExists(libPath); + + if (!shouldForceInstall && libExistsBeforeInstall) { + console.log(`Deps already installed for ${actualNetwork}/${upgradeId}; skipping.`); + return NextResponse.json( + { + success: true, + message: `Dependencies already installed for ${actualNetwork}/${upgradeId}`, + libExists: true, + installed: false, + depsInstalled: false, + stdout: '', + stderr: '', + }, + { status: 200 } + ); + } + console.log( `Installing dependencies for ${actualNetwork}/${upgradeId} (cwd: ${resolvedUpgradePath})` ); - await fs.rm(libPath, { recursive: true, force: true }); - const { stdout, stderr } = await execAsync(`make -f tasks/${upgradeId}/Makefile deps`, { cwd: resolvedUpgradePath, timeout: INSTALL_DEPS_TIMEOUT_MS, From 491a97fbe1e004f6c7f268c5b6fd63989f125ca8 Mon Sep 17 00:00:00 2001 From: Thanh Trinh Date: Tue, 4 Aug 2026 15:59:17 -0500 Subject: [PATCH 3/5] test: type dependency install command mock Co-authored-by: Codex --- src/app/api/install-deps/__tests__/route.test.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/app/api/install-deps/__tests__/route.test.ts b/src/app/api/install-deps/__tests__/route.test.ts index 955a5a9..29aee34 100644 --- a/src/app/api/install-deps/__tests__/route.test.ts +++ b/src/app/api/install-deps/__tests__/route.test.ts @@ -2,7 +2,13 @@ import { jest, describe, it, expect, beforeEach } from '@jest/globals'; import { NextRequest } from 'next/server'; const mockAccess = jest.fn<(path: string) => Promise>(); -const mockExecAsync = jest.fn<() => Promise<{ stdout: string; stderr: string }>>(); +const mockExecAsync = + jest.fn< + ( + command: string, + options: { cwd: string; timeout: number; env: NodeJS.ProcessEnv } + ) => Promise<{ stdout: string; stderr: string }> + >(); const mockFindContractDeploymentsRoot = jest.fn<() => string>(); jest.unstable_mockModule('fs', () => ({ From 06d75bffbc19615c395df971d5217a4c3ae60588 Mon Sep 17 00:00:00 2001 From: Thanh Trinh Date: Tue, 4 Aug 2026 16:11:38 -0500 Subject: [PATCH 4/5] test: keep selected task dependency check focused Co-authored-by: Codex --- src/app/api/install-deps/__tests__/route.test.ts | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/src/app/api/install-deps/__tests__/route.test.ts b/src/app/api/install-deps/__tests__/route.test.ts index 29aee34..db95df1 100644 --- a/src/app/api/install-deps/__tests__/route.test.ts +++ b/src/app/api/install-deps/__tests__/route.test.ts @@ -57,15 +57,4 @@ describe('POST /api/install-deps', () => { expect.objectContaining({ cwd: '/repo/active/evm' }) ); }); - - it('keeps existing shared libs', async () => { - mockAccess.mockResolvedValue(undefined); - - const response = await POST( - createRequest({ network: 'zeronet', upgradeId: '2026-07-10-transfer-owner' }) - ); - - expect(response.status).toBe(200); - expect(mockExecAsync).not.toHaveBeenCalled(); - }); }); From ac879b4d703c74065d4f03d0020f7b6ac88afce0 Mon Sep 17 00:00:00 2001 From: Thanh Trinh Date: Wed, 5 Aug 2026 02:04:49 -0500 Subject: [PATCH 5/5] refactor: run dependency install from task directory Co-authored-by: Codex --- .../api/install-deps/__tests__/route.test.ts | 60 ------------------- src/app/api/install-deps/route.ts | 16 ++--- 2 files changed, 6 insertions(+), 70 deletions(-) delete mode 100644 src/app/api/install-deps/__tests__/route.test.ts diff --git a/src/app/api/install-deps/__tests__/route.test.ts b/src/app/api/install-deps/__tests__/route.test.ts deleted file mode 100644 index db95df1..0000000 --- a/src/app/api/install-deps/__tests__/route.test.ts +++ /dev/null @@ -1,60 +0,0 @@ -import { jest, describe, it, expect, beforeEach } from '@jest/globals'; -import { NextRequest } from 'next/server'; - -const mockAccess = jest.fn<(path: string) => Promise>(); -const mockExecAsync = - jest.fn< - ( - command: string, - options: { cwd: string; timeout: number; env: NodeJS.ProcessEnv } - ) => Promise<{ stdout: string; stderr: string }> - >(); -const mockFindContractDeploymentsRoot = jest.fn<() => string>(); - -jest.unstable_mockModule('fs', () => ({ - promises: { access: mockAccess }, -})); - -jest.unstable_mockModule('util', () => ({ - promisify: () => mockExecAsync, -})); - -jest.unstable_mockModule('@/lib/deployments', () => ({ - findContractDeploymentsRoot: mockFindContractDeploymentsRoot, -})); - -const { POST } = await import('../route'); - -function createRequest(body: object): NextRequest { - return new NextRequest('http://localhost/api/install-deps', { - method: 'POST', - body: JSON.stringify(body), - headers: { 'content-type': 'application/json' }, - }); -} - -describe('POST /api/install-deps', () => { - beforeEach(() => { - jest.clearAllMocks(); - jest.spyOn(console, 'log').mockImplementation(() => {}); - mockFindContractDeploymentsRoot.mockReturnValue('/repo'); - mockExecAsync.mockResolvedValue({ stdout: 'installed', stderr: '' }); - }); - - it('installs missing shared libs with the selected task Makefile', async () => { - mockAccess - .mockResolvedValueOnce(undefined) - .mockRejectedValueOnce(new Error('missing')) - .mockResolvedValueOnce(undefined); - - const response = await POST( - createRequest({ network: 'zeronet', upgradeId: '2026-07-10-transfer-owner' }) - ); - - expect(response.status).toBe(200); - expect(mockExecAsync).toHaveBeenCalledWith( - 'make -f tasks/2026-07-10-transfer-owner/Makefile deps', - expect.objectContaining({ cwd: '/repo/active/evm' }) - ); - }); -}); diff --git a/src/app/api/install-deps/route.ts b/src/app/api/install-deps/route.ts index 52d40ef..3a7b8d6 100644 --- a/src/app/api/install-deps/route.ts +++ b/src/app/api/install-deps/route.ts @@ -32,6 +32,7 @@ export async function POST(req: NextRequest) { const actualNetwork = network.toLowerCase(); const shouldForceInstall = Boolean(forceInstall); + const safePathPattern = /^[a-zA-Z0-9_-]+$/; if (!safePathPattern.test(actualNetwork) || !safePathPattern.test(upgradeId)) { return NextResponse.json( @@ -57,16 +58,11 @@ export async function POST(req: NextRequest) { const libPath = path.join(resolvedUpgradePath, 'lib'); const resolvedTaskPath = assertWithinDir(taskPath, contractDeploymentsPath); - const taskMakefilePath = assertWithinDir( - path.join(resolvedTaskPath, 'Makefile'), - resolvedTaskPath - ); - if (!(await pathExists(taskMakefilePath))) { + const taskPathExists = await pathExists(resolvedTaskPath); + if (!taskPathExists) { return NextResponse.json( - { - error: `Task Makefile not found: ${path.relative(contractDeploymentsPath, taskMakefilePath)}`, - }, + { error: `Task folder not found: ${path.relative(contractDeploymentsPath, taskPath)}` }, { status: 404 } ); } @@ -93,8 +89,8 @@ export async function POST(req: NextRequest) { `Installing dependencies for ${actualNetwork}/${upgradeId} (cwd: ${resolvedUpgradePath})` ); - const { stdout, stderr } = await execAsync(`make -f tasks/${upgradeId}/Makefile deps`, { - cwd: resolvedUpgradePath, + const { stdout, stderr } = await execAsync('make deps', { + cwd: resolvedTaskPath, timeout: INSTALL_DEPS_TIMEOUT_MS, env: process.env, });