Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 27 additions & 32 deletions src/commands/actors/info.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { Actor, ActorChargeEvent, ActorTaggedBuild, Build, User } from 'apify-client';
import type { Actor, ActorChargeEvent, ActorTaggedBuild, ApifyApiError, ApifyClient, Build, User } from 'apify-client';
import chalk from 'chalk';

import { ApifyCommand } from '../../lib/command-framework/apify-command.js';
Expand Down Expand Up @@ -30,6 +30,19 @@ function formatEventPrice(price: number): string {
return `$${Number(price.toPrecision(2))}`;
}

async function getDefaultBuild(client: ApifyClient, actorId: string, fallback?: Build) {
try {
const build = await client.actor(actorId).defaultBuild();
return (await build.get()) ?? fallback;
} catch (err) {
if ((err as ApifyApiError).statusCode === 404) {
return fallback;
}

throw err;
}
}

const eventTitleColumn = '\u200b';
const eventPriceUsdColumn = '\u200b\u200b';

Expand Down Expand Up @@ -123,42 +136,24 @@ export class ActorsInfoCommand extends ApifyCommand<typeof ActorsInfoCommand> {
return;
}

const latest = actorInfo.taggedBuilds?.latest;

if (readme) {
if (!latest) {
error({
message: 'No README found for this Actor.',
stdout: true,
});

return;
}

if (!latest.build?.readme) {
error({
message: 'No README found for this Actor.',
stdout: true,
});
if (readme || input) {
const build = await getDefaultBuild(client, ctx.id, actorInfo.taggedBuilds?.latest?.build);

return;
}
if (readme) {
if (!build?.readme) {
error({
message: 'No README found for this Actor.',
stdout: true,
});

simpleLog({ message: latest.build.readme, stdout: true });
return;
}

if (input) {
if (!latest) {
error({
message: 'No input schema found for this Actor.',
stdout: true,
});
return;
}

simpleLog({ message: build.readme, stdout: true });
return;
}

if (!latest.build?.inputSchema) {
if (!build?.inputSchema) {
error({
message: 'No input schema found for this Actor.',
stdout: true,
Expand All @@ -167,7 +162,7 @@ export class ActorsInfoCommand extends ApifyCommand<typeof ActorsInfoCommand> {
return;
}

simpleLog({ message: latest.build.inputSchema, stdout: true });
simpleLog({ message: build.inputSchema, stdout: true });
return;
}

Expand Down
130 changes: 130 additions & 0 deletions test/local/commands/actors-info.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
import type { ApifyClient } from 'apify-client';

import { ActorsInfoCommand } from '../../../src/commands/actors/info.js';
import { testRunCommand } from '../../../src/lib/command-framework/apify-command.js';
import { useAuthSetup } from '../../__setup__/hooks/useAuthSetup.js';
import { useConsoleSpy } from '../../__setup__/hooks/useConsoleSpy.js';

const { mockGetLoggedClientOrThrow, mockGetCurrentUserInfo } = vitest.hoisted(() => ({
mockGetLoggedClientOrThrow: vitest.fn(),
mockGetCurrentUserInfo: vitest.fn(),
}));

vitest.mock('../../../src/lib/utils.js', async (importOriginal) => {
const original = await importOriginal<typeof import('../../../src/lib/utils.js')>();
return {
...original,
getLoggedClientOrThrow: mockGetLoggedClientOrThrow,
getCurrentUserInfo: mockGetCurrentUserInfo,
};
});

useAuthSetup();

const { logMessages } = useConsoleSpy();

const defaultBuild = {
buildNumber: '1.0.30',
readme: 'default build readme',
inputSchema: '{"title":"default build schema"}',
};

const fakeClient = ({
taggedBuilds,
defaultBuildResult,
defaultBuildError,
}: {
taggedBuilds: object;
defaultBuildResult?: object;
defaultBuildError?: Error;
}) =>
({
actor: () => ({
get: async () => ({
id: 'actorId',
name: 'some-actor',
username: 'someone-else',
userId: 'userId',
taggedBuilds,
defaultRunOptions: { build: 'version-1' },
}),
defaultBuild: async () => {
if (defaultBuildError) throw defaultBuildError;
if (!defaultBuildResult) throw Object.assign(new Error('Default build not found'), { statusCode: 404 });
return { get: async () => defaultBuildResult };
},
}),
user: () => ({ get: async () => ({ username: 'someone-else', profile: {} }) }),
build: () => ({ get: async () => ({ readme: 'tagged build readme', inputSchema: '{"title":"tagged"}' }) }),
}) as unknown as ApifyClient;

// The command framework records a thrown error on the real `process`; put it back so it cannot leak.
let originalExitCode: typeof process.exitCode;

beforeEach(() => {
originalExitCode = process.exitCode;
mockGetCurrentUserInfo.mockResolvedValue({ id: 'userId', username: 'someone-else' });
});

afterEach(() => {
process.exitCode = originalExitCode;
mockGetLoggedClientOrThrow.mockReset();
mockGetCurrentUserInfo.mockReset();
});

describe('apify actors info', () => {
it('prints the default build input schema when the Actor has no latest tag', async () => {
mockGetLoggedClientOrThrow.mockResolvedValue(
fakeClient({
taggedBuilds: { 'version-1': { buildId: 'b1', buildNumber: '1.0.30' } },
defaultBuildResult: defaultBuild,
}),
);

await testRunCommand(ActorsInfoCommand, { args_actorId: 'someone-else/some-actor', flags_input: true });

expect(logMessages.error).toEqual([]);
expect(logMessages.log.join('\n')).toBe(defaultBuild.inputSchema);
});

it('prefers the default build README over the latest tag', async () => {
mockGetLoggedClientOrThrow.mockResolvedValue(
fakeClient({
taggedBuilds: {
latest: { buildId: 'b0', buildNumber: '0.0.32' },
'version-1': { buildId: 'b1', buildNumber: '1.0.30' },
},
defaultBuildResult: defaultBuild,
}),
);

await testRunCommand(ActorsInfoCommand, { args_actorId: 'someone-else/some-actor', flags_readme: true });

expect(logMessages.log.join('\n')).toBe(defaultBuild.readme);
});

it('falls back to the latest tag when the default build cannot be resolved', async () => {
mockGetLoggedClientOrThrow.mockResolvedValue(
fakeClient({ taggedBuilds: { latest: { buildId: 'b0', buildNumber: '0.0.32' } } }),
);

await testRunCommand(ActorsInfoCommand, { args_actorId: 'someone-else/some-actor', flags_input: true });

expect(logMessages.log.join('\n')).toBe('{"title":"tagged"}');
});

it('does not fall back to the latest tag when the default build lookup fails with a non-404 error', async () => {
mockGetLoggedClientOrThrow.mockResolvedValue(
fakeClient({
taggedBuilds: { latest: { buildId: 'b0', buildNumber: '0.0.32' } },
defaultBuildError: Object.assign(new Error('Internal error'), { statusCode: 500 }),
}),
);

await testRunCommand(ActorsInfoCommand, { args_actorId: 'someone-else/some-actor', flags_input: true });

expect(process.exitCode).toBe(1);
expect(logMessages.error.join('\n')).toContain('Internal error');
expect(logMessages.log.join('\n')).not.toContain('tagged');
});
});
Loading