From 229b1774833c56d363610170db77a81ea761b32b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 6 Apr 2026 21:08:47 +0000 Subject: [PATCH 1/3] Add failing tests for stale config edge cases after aioConfigLoader.reload() Three edge cases identified: 1. config.ow.auth passed to deployActions is stale when pre-deploy hook modifies .env 2. this.appConfig cache not cleared after reload 3. Multi-extension deploys all use stale configs from initial load Agent-Logs-Url: https://github.com/adobe/aio-cli-plugin-app/sessions/96f9b49e-4a86-4996-959a-b15807c820b4 Co-authored-by: purplecabbage <46134+purplecabbage@users.noreply.github.com> --- test/commands/app/deploy.test.js | 119 +++++++++++++++++++++++++++++++ 1 file changed, 119 insertions(+) diff --git a/test/commands/app/deploy.test.js b/test/commands/app/deploy.test.js index 64c65435..544a0ea1 100644 --- a/test/commands/app/deploy.test.js +++ b/test/commands/app/deploy.test.js @@ -887,6 +887,125 @@ describe('run', () => { ) }) + test('deploy should pass refreshed config to deployActions when pre-deploy hook changes env', async () => { + const staleAuth = 'stale-auth-token' + const freshAuth = 'fresh-auth-from-hook' + + // Initial config loaded before hooks run has stale auth + const staleAppConfig = createAppConfig({ + ...command.appConfig, + runtime: { auth: staleAuth, namespace: 'test-ns' } + }) + + // After reload, the config should be re-derived with the fresh auth + const freshAppConfig = createAppConfig({ + ...command.appConfig, + runtime: { auth: freshAuth, namespace: 'test-ns' } + }) + + // First call returns stale config (initial load), second call returns fresh config (after reload) + command.getAppExtConfigs + .mockResolvedValueOnce(staleAppConfig) + .mockResolvedValueOnce(freshAppConfig) + + // Pre-deploy hook simulates writing a new auth token to .env + helpers.runInProcess + .mockImplementationOnce(async () => { + // In real code, a hook might write new credentials to .env + process.env.AIO_RUNTIME_AUTH = freshAuth + return undefined // no script found + }) + .mockResolvedValueOnce(undefined) // deploy-actions + .mockResolvedValueOnce(undefined) // post-app-deploy + + command.argv = ['--no-web-assets'] + await command.run() + + expect(mockRuntimeLib.deployActions).toHaveBeenCalledTimes(1) + const deployedConfig = mockRuntimeLib.deployActions.mock.calls[0][0] + // The config passed to deployActions should reflect the refreshed auth, + // not the stale value from the initial config load + expect(deployedConfig.ow.auth).toBe(freshAuth) + + delete process.env.AIO_RUNTIME_AUTH + }) + + test('deploy should clear appConfig cache after reload so subsequent loads are fresh', async () => { + const staleAppConfig = createAppConfig({ + ...command.appConfig, + runtime: { auth: 'stale-auth', namespace: 'stale-ns' } + }) + const freshAppConfig = createAppConfig({ + ...command.appConfig, + runtime: { auth: 'fresh-auth', namespace: 'fresh-ns' } + }) + + command.getAppExtConfigs + .mockResolvedValueOnce(staleAppConfig) + .mockResolvedValueOnce(freshAppConfig) + + helpers.runInProcess + .mockResolvedValueOnce(undefined) // pre-app-deploy + .mockResolvedValueOnce(undefined) // deploy-actions + .mockResolvedValueOnce(undefined) // post-app-deploy + + command.argv = ['--no-web-assets'] + await command.run() + + // After deploy with reload, the appConfig cache should be cleared + // so that a subsequent getAppExtConfigs call returns fresh config. + // This matters for multi-extension loops and post-deploy logic. + expect(command.appConfig).toBeNull() + }) + + test('deploy should pass refreshed config for each extension in multi-extension deploy', async () => { + const staleAuth = 'stale-auth-multi' + const freshAuth = 'fresh-auth-multi' + + const staleMultiConfig = createAppConfig({ + ...command.appConfig, + runtime: { auth: staleAuth, namespace: 'test-ns' } + }, 'app-exc-nui') + + // Fresh config returned after each reload + const freshMultiConfig = createAppConfig({ + ...command.appConfig, + runtime: { auth: freshAuth, namespace: 'test-ns' } + }, 'app-exc-nui') + + // Initial load returns stale config, then each per-extension reload returns fresh config + command.getAppExtConfigs + .mockResolvedValueOnce(staleMultiConfig) // initial load in run() + .mockResolvedValue(freshMultiConfig) // all subsequent reload calls + + // Each extension: pre-app-deploy, deploy-actions, post-app-deploy + // app-exc-nui has 3 extensions (application, dx/asset-compute/worker/1, dx/excshell/1) + // First extension's pre-deploy hook changes env + helpers.runInProcess + .mockImplementationOnce(async () => { + // First extension's pre-deploy hook modifies .env + process.env.AIO_RUNTIME_AUTH = freshAuth + return undefined + }) + .mockResolvedValue(undefined) // all other hook calls return undefined + + mockExtRegExcShellAndNuiPayload() + command.argv = ['--no-web-assets'] + await command.run() + + // All 3 extensions should have been deployed + expect(mockRuntimeLib.deployActions).toHaveBeenCalledTimes(3) + + // The second and third extension deploys should use the fresh auth, + // not the stale auth from the initial config load + const secondExtConfig = mockRuntimeLib.deployActions.mock.calls[1][0] + const thirdExtConfig = mockRuntimeLib.deployActions.mock.calls[2][0] + expect(secondExtConfig.ow.auth).toBe(freshAuth) + expect(thirdExtConfig.ow.auth).toBe(freshAuth) + + delete process.env.AIO_RUNTIME_AUTH + }) + test('deploy (has deploy-actions and deploy-static hooks)', async () => { command.getAppExtConfigs.mockResolvedValueOnce(createAppConfig(command.appConfig)) const noScriptFound = undefined From 5e31357f31bb4d87d20780dbde10901493abbd3d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 6 Apr 2026 21:10:18 +0000 Subject: [PATCH 2/3] Fix stale config: re-derive app config after aioConfigLoader.reload() After aioConfigLoader.reload(), clear the appConfig cache and re-load the extension config so that env changes from hooks (e.g. new credentials written to .env) are reflected in all config values like ow.auth and ow.namespace, not just process.env used by input resolution. Agent-Logs-Url: https://github.com/adobe/aio-cli-plugin-app/sessions/96f9b49e-4a86-4996-959a-b15807c820b4 Co-authored-by: purplecabbage <46134+purplecabbage@users.noreply.github.com> --- src/commands/app/deploy.js | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/commands/app/deploy.js b/src/commands/app/deploy.js index bc275c4d..3ed48552 100644 --- a/src/commands/app/deploy.js +++ b/src/commands/app/deploy.js @@ -232,6 +232,14 @@ class Deploy extends BuildCommand { this.error(hookResults.failures.map(f => `${f.plugin.name} : ${f.error.message}`).join('\nError: '), { exit: 1 }) } aioConfigLoader.reload() + // Re-derive the app config so that env changes from hooks + // (e.g. new credentials written to .env) are reflected in all + // config values, not just process.env used by input resolution. + this.appConfig = null + const refreshedConfigs = await this.getAppExtConfigs(flags) + if (refreshedConfigs[name]) { + config = { ...setRuntimeApiHostAndAuthHandler(refreshedConfigs[name]) } + } deployedRuntimeEntities = await rtLib.deployActions(config, { filterEntities, useForce: flags['force-deploy'] }, onProgress) } From ebf015c230be13261436a26926cda69c43559299 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 6 Apr 2026 21:12:25 +0000 Subject: [PATCH 3/3] Wrap env-modifying tests in try/finally for reliable cleanup Agent-Logs-Url: https://github.com/adobe/aio-cli-plugin-app/sessions/96f9b49e-4a86-4996-959a-b15807c820b4 Co-authored-by: purplecabbage <46134+purplecabbage@users.noreply.github.com> --- test/commands/app/deploy.test.js | 48 +++++++++++++++++--------------- 1 file changed, 26 insertions(+), 22 deletions(-) diff --git a/test/commands/app/deploy.test.js b/test/commands/app/deploy.test.js index 544a0ea1..27f3ec29 100644 --- a/test/commands/app/deploy.test.js +++ b/test/commands/app/deploy.test.js @@ -919,15 +919,17 @@ describe('run', () => { .mockResolvedValueOnce(undefined) // post-app-deploy command.argv = ['--no-web-assets'] - await command.run() - - expect(mockRuntimeLib.deployActions).toHaveBeenCalledTimes(1) - const deployedConfig = mockRuntimeLib.deployActions.mock.calls[0][0] - // The config passed to deployActions should reflect the refreshed auth, - // not the stale value from the initial config load - expect(deployedConfig.ow.auth).toBe(freshAuth) - - delete process.env.AIO_RUNTIME_AUTH + try { + await command.run() + + expect(mockRuntimeLib.deployActions).toHaveBeenCalledTimes(1) + const deployedConfig = mockRuntimeLib.deployActions.mock.calls[0][0] + // The config passed to deployActions should reflect the refreshed auth, + // not the stale value from the initial config load + expect(deployedConfig.ow.auth).toBe(freshAuth) + } finally { + delete process.env.AIO_RUNTIME_AUTH + } }) test('deploy should clear appConfig cache after reload so subsequent loads are fresh', async () => { @@ -991,19 +993,21 @@ describe('run', () => { mockExtRegExcShellAndNuiPayload() command.argv = ['--no-web-assets'] - await command.run() - - // All 3 extensions should have been deployed - expect(mockRuntimeLib.deployActions).toHaveBeenCalledTimes(3) - - // The second and third extension deploys should use the fresh auth, - // not the stale auth from the initial config load - const secondExtConfig = mockRuntimeLib.deployActions.mock.calls[1][0] - const thirdExtConfig = mockRuntimeLib.deployActions.mock.calls[2][0] - expect(secondExtConfig.ow.auth).toBe(freshAuth) - expect(thirdExtConfig.ow.auth).toBe(freshAuth) - - delete process.env.AIO_RUNTIME_AUTH + try { + await command.run() + + // All 3 extensions should have been deployed + expect(mockRuntimeLib.deployActions).toHaveBeenCalledTimes(3) + + // The second and third extension deploys should use the fresh auth, + // not the stale auth from the initial config load + const secondExtConfig = mockRuntimeLib.deployActions.mock.calls[1][0] + const thirdExtConfig = mockRuntimeLib.deployActions.mock.calls[2][0] + expect(secondExtConfig.ow.auth).toBe(freshAuth) + expect(thirdExtConfig.ow.auth).toBe(freshAuth) + } finally { + delete process.env.AIO_RUNTIME_AUTH + } }) test('deploy (has deploy-actions and deploy-static hooks)', async () => {