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
40 changes: 36 additions & 4 deletions src/cli/operations/deploy/__tests__/change-detection.test.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,35 @@
import { canSkipDeploy, computeProjectDeployHash } from '../change-detection';
import { describe, expect, it, vi } from 'vitest';
import { beforeEach, describe, expect, it, vi } from 'vitest';

const mockFiles = vi.hoisted(() => ({
dockerfile: 'FROM public.ecr.aws/lambda/nodejs:20\n',
harnessJson: JSON.stringify({
name: 'h1',
dockerfile: 'Dockerfile',
model: { provider: 'bedrock', modelId: 'anthropic.claude-3' },
}),
prompt: 'You are a helpful assistant.',
}));

vi.mock('node:fs/promises', () => ({
readFile: vi.fn().mockImplementation((path: string) => {
if (path.includes('harness.json'))
return Promise.resolve('{"name":"h1","model":{"provider":"bedrock","modelId":"anthropic.claude-3"}}');
if (path.includes('system-prompt.md')) return Promise.resolve('You are a helpful assistant.');
if (path.includes('harness.json')) return Promise.resolve(mockFiles.harnessJson);
if (path.includes('system-prompt.md')) return Promise.resolve(mockFiles.prompt);
if (path.includes('Dockerfile')) return Promise.resolve(mockFiles.dockerfile);
return Promise.reject(Object.assign(new Error('ENOENT'), { code: 'ENOENT' }));
}),
}));

beforeEach(() => {
mockFiles.dockerfile = 'FROM public.ecr.aws/lambda/nodejs:20\n';
mockFiles.harnessJson = JSON.stringify({
name: 'h1',
dockerfile: 'Dockerfile',
model: { provider: 'bedrock', modelId: 'anthropic.claude-3' },
});
mockFiles.prompt = 'You are a helpful assistant.';
});

function mockConfigIO(opts: {
runtimes?: any[];
harnesses?: any[];
Expand Down Expand Up @@ -52,6 +72,18 @@ describe('computeProjectDeployHash', () => {
const hash2 = await computeProjectDeployHash(io2);
expect(hash1).not.toBe(hash2);
});

it('returns different hash when harness Dockerfile content changes', async () => {
const io = mockConfigIO({});

mockFiles.dockerfile = 'FROM public.ecr.aws/lambda/nodejs:20\nRUN echo first\n';
const hash1 = await computeProjectDeployHash(io);

mockFiles.dockerfile = 'FROM public.ecr.aws/lambda/nodejs:20\nRUN echo second\n';
const hash2 = await computeProjectDeployHash(io);

expect(hash1).not.toBe(hash2);
});
});

describe('canSkipDeploy', () => {
Expand Down
13 changes: 13 additions & 0 deletions src/cli/operations/deploy/change-detection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,19 @@ export async function computeProjectDeployHash(configIO: ConfigIO): Promise<stri
try {
const harnessJson = await readFile(join(harnessDir, 'harness.json'), 'utf-8');
hash.update(harnessJson);
try {
const harnessSpec = JSON.parse(harnessJson) as { dockerfile?: unknown };
if (typeof harnessSpec.dockerfile === 'string' && harnessSpec.dockerfile.length > 0) {
try {
const dockerfileContent = await readFile(join(harnessDir, harnessSpec.dockerfile), 'utf-8');
hash.update(dockerfileContent);
} catch {
// Dockerfile missing — preflight validates existence before deploy.
}
}
} catch {
// Invalid harness.json is still represented by the raw content above.
}
} catch {
// harness.json missing — hash will differ from last deploy
}
Expand Down
Loading