Skip to content
Merged
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
15 changes: 11 additions & 4 deletions src/commands/cloudmanager/environment/tail-log.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,10 +31,17 @@ class TailLog extends BaseCommand {
}

async tailLog (programId, environmentId, service, logName, imsContextName = null) {
return executeWithRetries(async () => {
const sdk = await initSdk(imsContextName)
return sdk.tailLog(programId, environmentId, service, logName, process.stdout)
})
while (true) {
try {
await executeWithRetries(async () => {
const sdk = await initSdk(imsContextName)
return sdk.tailLog(programId, environmentId, service, logName, process.stdout)
})
} catch (error) {
if (error.code) throw error
}
await new Promise(resolve => setTimeout(resolve, 2000))
}
}
}

Expand Down
64 changes: 63 additions & 1 deletion test/commands/environment/tail-log.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,11 +37,22 @@ test('tail-log - missing config', async () => {
test('tail-log - config', async () => {
setCurrentOrgId('good')

// Block on the second tailLog call so the reconnect loop doesn't run indefinitely.
// The 2000ms reconnect delay means the second call won't happen within our 50ms window.
let callCount = 0
mockSdk.tailLog.mockImplementation(() => {
callCount++
return callCount === 1 ? Promise.resolve() : new Promise(() => {})
})

expect.assertions(5)

const runResult = TailLog.run(['17', 'author', 'aemerror', '--programId', '5'])
await expect(runResult instanceof Promise).toBeTruthy()
await runResult
// Don't await runResult — the reconnect loop never terminates.
// Wait 50ms for the first sdk.tailLog() call to complete (the reconnect delay is 2000ms,
// so the second call hasn't been made yet).
await new Promise(resolve => setTimeout(resolve, 50))
await expect(init.mock.calls.length).toEqual(1)
await expect(init).toHaveBeenCalledWith('good', 'test-client-id', 'fake-token', 'https://cloudmanager.adobe.io')
await expect(mockSdk.tailLog.mock.calls.length).toEqual(1)
Expand All @@ -58,3 +69,54 @@ test('tail-log - should retry 5 times and throw error', async () => {
await expect(runResult).rejects.toThrow('[CloudManagerCLI:MAX_RETRY_REACHED] Max retries reached')
await expect(mockSdk.tailLog.mock.calls.length).toEqual(5)
})

test('tail-log - reconnects after normal stream end', async () => {
setCurrentOrgId('good')
mockSdk.tailLog.mockClear()

let callCount = 0
mockSdk.tailLog.mockImplementation(() => {
callCount++
// First two calls resolve immediately (simulating EOF / end of stream)
// Third call blocks forever (simulating an active stream)
return callCount < 3 ? Promise.resolve() : new Promise(() => {})
})

TailLog.run(['17', 'author', 'aemerror', '--programId', '5'])

// Wait long enough for the 2000ms reconnect delay to fire at least once
await new Promise(resolve => setTimeout(resolve, 2500))

expect(mockSdk.tailLog.mock.calls.length).toBeGreaterThanOrEqual(2)
}, 10000)

test('tail-log - retries silently on transient non-auth error', async () => {
setCurrentOrgId('good')
mockSdk.tailLog.mockClear()

let callCount = 0
mockSdk.tailLog.mockImplementation(() => {
callCount++
if (callCount === 1) {
// Simulate a 404 (log stream not ready yet — as seen during the midnight rotation window)
return Promise.reject(Object.assign(new Error('Not Found'), {
sdkDetails: { response: { status: 404 } },
}))
}
// Second call blocks (active stream established)
return new Promise(() => {})
})

const runResult = TailLog.run(['17', 'author', 'aemerror', '--programId', '5'])

// Wait for the 2000ms reconnect delay to fire after the first rejected call
await new Promise(resolve => setTimeout(resolve, 2500))

// The CLI must have retried (2nd call made)
expect(mockSdk.tailLog.mock.calls.length).toBeGreaterThanOrEqual(2)

// The CLI must NOT have rejected (no crash on transient error)
await expect(
Promise.race([runResult.then(() => 'resolved'), Promise.resolve('still-running')]),
).resolves.toBe('still-running')
}, 10000)
Loading