chore(core): Log npm output when the sandbox workspace link install fails (#40591)

Co-authored-by: Claude Opus 5.5 <[email protected]>
This commit is contained in:
Jaakko Husso
2026-10-08 09:26:41 +00:00
committed by GitHub
co-authored by Claude Opus 5.5
parent f212d5b900
commit 1d63a8f3a1
2 changed files with 100 additions and 7 deletions
@@ -847,6 +847,87 @@ describe('setupSandboxWorkspace', () => {
'/workspace',
);
});
it('reports the install output when the tarball install fails', async () => {
const packSandboxLinkedWorkspacePackages = vi.fn().mockResolvedValue([
{
filename: 'workflow-sdk.tgz',
tarball: Buffer.from('sdk'),
version: '1.0.0',
packageName: '@n8n/workflow-sdk',
packagePath: '/host/sdk',
},
]);
const npmError = 'npm error notarget No matching version found for [email protected].';
const runInSandbox: RunInSandboxMock = vi
.fn<
(
...args: [SandboxWorkspace, string, string?]
) => Promise<{ exitCode: number; stdout: string; stderr: string }>
>()
// Daytona returns all command output in stdout.
.mockResolvedValue({ exitCode: 1, stdout: npmError, stderr: '' });
const linkWorkspaceSdkIfEnabled = await loadLinkWorkspaceSdkWithMocks(
packSandboxLinkedWorkspacePackages,
runInSandbox,
);
const workspace = {
filesystem: { provider: 'daytona', writeFile: vi.fn(async () => {}) },
sandbox: {},
} as unknown as SandboxWorkspace;
const logger = { error: vi.fn(), info: vi.fn(), warn: vi.fn(), debug: vi.fn() };
await expect(linkWorkspaceSdkIfEnabled(workspace, '/workspace', logger)).rejects.toThrow(
`Failed to install workspace package tarballs (exit code 1): ${npmError}`,
);
expect(logger.error).toHaveBeenCalledWith(
'Failed to link workspace packages into sandbox',
expect.objectContaining({
exitCode: 1,
command: expect.stringContaining(
"npm install '/workspace/workflow-sdk.tgz' --no-save --force",
),
output: npmError,
}),
);
});
it('keeps the end of long install output with stderr last', async () => {
const packSandboxLinkedWorkspacePackages = vi.fn().mockResolvedValue([
{
filename: 'workflow-sdk.tgz',
tarball: Buffer.from('sdk'),
version: '1.0.0',
packageName: '@n8n/workflow-sdk',
packagePath: '/host/sdk',
},
]);
const stdout = 'npm http fetch GET 200 https://registry.npmjs.org/pkg\n'.repeat(200);
const stderr = 'npm error notarget No matching version found for [email protected].';
const runInSandbox: RunInSandboxMock = vi
.fn<
(
...args: [SandboxWorkspace, string, string?]
) => Promise<{ exitCode: number; stdout: string; stderr: string }>
>()
.mockResolvedValue({ exitCode: 1, stdout, stderr });
const linkWorkspaceSdkIfEnabled = await loadLinkWorkspaceSdkWithMocks(
packSandboxLinkedWorkspacePackages,
runInSandbox,
);
const workspace = {
filesystem: { provider: 'local', writeFile: vi.fn(async () => {}) },
sandbox: {},
} as unknown as SandboxWorkspace;
const logger = { error: vi.fn(), info: vi.fn(), warn: vi.fn(), debug: vi.fn() };
await expect(linkWorkspaceSdkIfEnabled(workspace, '/workspace', logger)).rejects.toThrow(
stderr,
);
const { output } = logger.error.mock.calls[0][1] as { output: string };
const expectedTail = `${stdout}\n${stderr}`.trim().slice(-4000);
expect(output).toBe(`…${expectedTail}`);
});
});
describe('formatNodeCatalogLine', () => {
let formatNodeCatalogLine: FormatNodeCatalogLine;
@@ -233,6 +233,15 @@ export const PACKAGE_JSON = buildPackageJson(
let linkedPackagesPromise: Promise<WorkspacePackageTarball[] | null> | null = null;
const MAX_INSTALL_OUTPUT_CHARS = 4000;
function tailOutput(output: string): string {
const trimmed = output.trim();
return trimmed.length > MAX_INSTALL_OUTPUT_CHARS
? `…${trimmed.slice(-MAX_INSTALL_OUTPUT_CHARS)}`
: trimmed;
}
export async function linkWorkspaceSdkIfEnabled(
workspace: SandboxWorkspace,
root: string,
@@ -266,17 +275,20 @@ export async function linkWorkspaceSdkIfEnabled(
const tarballArgs = remotePaths
.map((remotePath) => `'${escapeSingleQuotes(remotePath)}'`)
.join(' ');
const install = await runInSandbox(
workspace,
`npm install ${tarballArgs} --no-save --force ${resolveNpmInstallFlags(workspace)}`,
root,
);
const command = `npm install ${tarballArgs} --no-save --force ${resolveNpmInstallFlags(workspace)}`;
const install = await runInSandbox(workspace, command, root);
if (install.exitCode !== 0) {
// Daytona returns all command output in stdout, so stderr alone is empty there.
// npm prints its error summary to stderr, so put stderr last and keep the end of the output.
const output = tailOutput([install.stdout, install.stderr].filter(Boolean).join('\n'));
logger.error('Failed to link workspace packages into sandbox', {
exitCode: install.exitCode,
stderr: install.stderr,
command,
output,
});
throw new Error(`Failed to install workspace package tarballs: ${install.stderr}`);
throw new Error(
`Failed to install workspace package tarballs (exit code ${install.exitCode}): ${output}`,
);
}
logger.info('Linked workspace packages into sandbox', {