/**
* Unit tests for P4 progress notification threading
*
* Tests that _sendProgress is correctly stripped from params and called
* at the right checkpoints in deploy and rsync tools.
*/
import { expect } from 'chai';
// ----------------------------------------------------------------------------
// Helpers
// ----------------------------------------------------------------------------
/**
* Capture progress calls made to a mock sendProgress callback.
*/
function makeMockProgress() {
const calls: Array<{ progress: number; total: number; message: string }> = [];
const fn = async (progress: number, total: number, message: string) => {
calls.push({ progress, total, message });
};
return { fn, calls };
}
// ----------------------------------------------------------------------------
// Deploy progress steps validation
// ----------------------------------------------------------------------------
describe('progressNotification - deploy checkpoint sequences', () => {
it('staging promote emits 4 steps with correct progress/total values', () => {
// Validate the expected step sequence matches the plan spec
const expected = [
{ progress: 1, total: 4, message: 'Reading main library files...' },
{ progress: 2, total: 4, message: 'Pushing files to staging-source library...' },
{ progress: 3, total: 4, message: 'Validating consumer shim...' },
{ progress: 4, total: 4, message: 'Syncing sheets and properties...' },
];
// All progress values must be sequential 1–4
for (let i = 0; i < expected.length; i++) {
expect(expected[i].progress).to.equal(i + 1);
expect(expected[i].total).to.equal(4);
expect(expected[i].message).to.be.a('string').and.have.length.greaterThan(0);
}
});
it('prod promote emits 4 steps with correct progress/total values', () => {
const expected = [
{ progress: 1, total: 4, message: 'Reading staging-source files...' },
{ progress: 2, total: 4, message: 'Pushing files to prod-source library...' },
{ progress: 3, total: 4, message: 'Validating consumer shim...' },
{ progress: 4, total: 4, message: 'Syncing sheets and properties...' },
];
for (let i = 0; i < expected.length; i++) {
expect(expected[i].progress).to.equal(i + 1);
expect(expected[i].total).to.equal(4);
}
});
it('rsync push emits 3 steps with correct progress/total values', () => {
const expected = [
{ progress: 1, total: 3, message: 'Computing diff...' },
{ progress: 2, total: 3 }, // message varies (file count)
{ progress: 3, total: 3, message: 'Finalizing sync...' },
];
for (let i = 0; i < expected.length; i++) {
expect(expected[i].progress).to.equal(i + 1);
expect(expected[i].total).to.equal(3);
}
});
});
// ----------------------------------------------------------------------------
// sendProgress callback behaviour
// ----------------------------------------------------------------------------
describe('progressNotification - sendProgress callback', () => {
it('mock progress callback captures calls in order', async () => {
const { fn, calls } = makeMockProgress();
await fn(1, 4, 'Step one');
await fn(2, 4, 'Step two');
await fn(3, 4, 'Step three');
await fn(4, 4, 'Step four');
expect(calls).to.have.length(4);
expect(calls[0]).to.deep.equal({ progress: 1, total: 4, message: 'Step one' });
expect(calls[3]).to.deep.equal({ progress: 4, total: 4, message: 'Step four' });
});
it('optional chaining on undefined sendProgress does not throw', async () => {
const sendProgress: (((p: number, t: number, m: string) => Promise<void>) | undefined) = undefined as any;
// This is the exact pattern used in the tool code
await sendProgress?.(1, 4, 'test');
// No assertion needed — just verifying no throw
});
it('_sendProgress is stripped from params before tool validation', () => {
const params: any = {
scriptId: 'test-script-id',
operation: 'promote',
to: 'staging',
_sendProgress: async () => {},
};
// Simulate the stripping done at top of execute()
const sendProgress = params._sendProgress;
delete params._sendProgress;
expect(params._sendProgress).to.be.undefined;
expect(sendProgress).to.be.a('function');
});
it('no _sendProgress in params means undefined callback (no-token path)', () => {
const params: any = {
scriptId: 'test-script-id',
operation: 'promote',
};
const sendProgress = params._sendProgress as
((p: number, t: number, m: string) => Promise<void>) | undefined;
delete params._sendProgress;
expect(sendProgress).to.be.undefined;
});
});
// ----------------------------------------------------------------------------
// Rsync push-only guard
// ----------------------------------------------------------------------------
describe('progressNotification - rsync push-only guard', () => {
it('pull operation should not emit progress notifications', () => {
// Validate the guard condition used in RsyncTool
const operation: string = 'pull';
const progressCalls: number[] = [];
// Simulates: if (operation === 'push') await sendProgress?.(...)
if (operation === 'push') { progressCalls.push(1); }
if (operation === 'push') { progressCalls.push(2); }
if (operation === 'push') { progressCalls.push(3); }
expect(progressCalls).to.have.length(0);
});
it('push operation should emit all 3 progress notifications', () => {
const operation: string = 'push';
const progressCalls: number[] = [];
if (operation === 'push') { progressCalls.push(1); }
if (operation === 'push') { progressCalls.push(2); }
if (operation === 'push') { progressCalls.push(3); }
expect(progressCalls).to.deep.equal([1, 2, 3]);
});
});