mirror of
https://github.com/n8n-io/n8n.git
synced 2026-10-12 02:19:43 +00:00
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:
co-authored by
Claude Opus 5.5
parent
f212d5b900
commit
1d63a8f3a1
@@ -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', {
|
||||
|
||||
Reference in New Issue
Block a user