fix(core): Preserve local credential fields absent from source contro… (#34173)

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Sandra Zollner 2026-07-16 10:51:30 +02:00 committed by GitHub
parent fcdd756122
commit 0a4dddeb29
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 32 additions and 10 deletions

View File

@ -1188,7 +1188,7 @@ describe('Source Control Helper', () => {
expect(result.port).toBe(5000); // Number from remote
});
it('should only include fields present in remote', () => {
it('should keep local fields that are absent from remote', () => {
const local = {
apiKey: 'secret',
port: 3000,
@ -1197,14 +1197,32 @@ describe('Source Control Helper', () => {
const remote = {
port: 8080,
apiKey: '', // Plain string sanitized to empty
// extraField is NOT in remote, so won't be in result
// extraField is NOT in remote (e.g. left at its default when pushed)
};
const result = mergeRemoteCrendetialDataIntoLocalCredentialData({ local, remote });
expect(result.apiKey).toBe('secret'); // Local preserved (empty skipped)
expect(result.port).toBe(8080); // Merged from remote
expect(result.extraField).toBeUndefined(); // Not in remote, not in result
expect(result.extraField).toBe('local-only'); // Absent from remote, local retained
});
it('should not reset a locally selected option field the remote stub omits', () => {
// A stub pushed from an instance where the option was left at its default
// omits that field entirely. Pulling it must not reset a different local selection.
const local = {
apikey: 'prod-secret',
environment: 'https://api.example.com', // non-default selection on this instance
};
const remote = {
apikey: '', // secret blanked in the stub
// environment omitted because the pushing instance used the default
} as ICredentialDataDecryptedObject;
const result = mergeRemoteCrendetialDataIntoLocalCredentialData({ local, remote });
expect(result.apikey).toBe('prod-secret');
expect(result.environment).toBe('https://api.example.com');
});
it('should handle empty local object', () => {
@ -1229,8 +1247,8 @@ describe('Source Control Helper', () => {
const result = mergeRemoteCrendetialDataIntoLocalCredentialData({ local, remote });
// Remote is empty, so result is empty (remote is source of truth for which fields exist)
expect(result).toEqual({});
// Remote carries no fields, so every local field is retained rather than wiped
expect(result).toEqual({ apiKey: 'secret', port: 3000 });
});
it('should handle both empty objects', () => {

View File

@ -141,11 +141,15 @@ export function mergeRemoteCrendetialDataIntoLocalCredentialData({
}
}
// Because oauthTokenData is explicitly stripped from the remote data during sanitization,
// it will never exist in the sanitizedRemote object. Therefore, it is skipped in the loop above.
// We manually merge it back from local to prevent OAuth credentials being wiped out on pull.
if (local.oauthTokenData) {
merged.oauthTokenData = local.oauthTokenData;
// Keep local fields the remote stub does not carry. A field left at its default value
// is not persisted, so it never reaches the stub; an absent field carries the same
// "no value to give" meaning as a present-but-blank one, which is already preserved
// above. Without this it would be dropped on pull and reset to its default. This also
// covers oauthTokenData, which sanitization always strips from the remote.
for (const [key, localValue] of Object.entries(local)) {
if (!(key in sanitizedRemote)) {
merged[key] = localValue;
}
}
return merged;