From 0c1c6f7ca097d848332bc4647a95571d66e9f991 Mon Sep 17 00:00:00 2001 From: openhands Date: Sat, 11 Apr 2026 10:46:44 +0000 Subject: [PATCH 1/3] =?UTF-8?q?Fix=20issue=20#20:=20Cache=20f=C3=BCr=20Jir?= =?UTF-8?q?a-Personennamen=20mapping?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- angular.json | 3 +- src/app/jira/jira.service.spec.ts | 88 +++++++++++++++++++++++++++++++ src/app/jira/jira.service.ts | 41 +++++++++++++- 3 files changed, 130 insertions(+), 2 deletions(-) diff --git a/angular.json b/angular.json index 9eee768..c65c792 100644 --- a/angular.json +++ b/angular.json @@ -2,7 +2,8 @@ "$schema": "./node_modules/@angular/cli/lib/config/schema.json", "version": 1, "cli": { - "packageManager": "npm" + "packageManager": "npm", + "analytics": false }, "newProjectRoot": "projects", "projects": { diff --git a/src/app/jira/jira.service.spec.ts b/src/app/jira/jira.service.spec.ts index d5b46d2..8b4b548 100644 --- a/src/app/jira/jira.service.spec.ts +++ b/src/app/jira/jira.service.spec.ts @@ -9,6 +9,9 @@ describe('JiraService', () => { let httpMock: HttpTestingController; beforeEach(() => { + // Clear localStorage before each test to ensure clean state + localStorage.removeItem('jira.userDisplayNameCache'); + TestBed.configureTestingModule({ providers: [provideHttpClient(withFetch()), provideHttpClientTesting()], }); @@ -369,4 +372,89 @@ describe('JiraService', () => { .flush(makeRawIssue({ description: 'Kein Erwähnungen hier.' })); httpMock.expectNone((r) => r.url.includes('/user')); }); + + it('loads user display name cache from localStorage on service initialization', () => { + // Clear localStorage first + localStorage.removeItem('jira.userDisplayNameCache'); + + // Setup localStorage with cached data + const cachedData = { 'u1': 'User One', 'u2': 'User Two' }; + localStorage.setItem('jira.userDisplayNameCache', JSON.stringify(cachedData)); + + // Create a new service instance + const newService = TestBed.inject(JiraService); + + // Verify cache was loaded + expect(newService['userDisplayNameCache'].size).toBe(2); + expect(newService['userDisplayNameCache'].get('u1')).toBe('User One'); + expect(newService['userDisplayNameCache'].get('u2')).toBe('User Two'); + + // Clean up + localStorage.removeItem('jira.userDisplayNameCache'); + }); + + it('saves resolved user display names to localStorage', () => { + // Clear localStorage first + localStorage.removeItem('jira.userDisplayNameCache'); + + let result: JiraTicket | undefined; + service.getTicketByKey('TEST-1').subscribe((t) => (result = t)); + + httpMock + .expectOne((r) => r.url.includes('/issue/TEST-1')) + .flush(makeRawIssue({ description: '[~newuser]' })); + httpMock + .expectOne((r) => r.url.includes('/user') && r.params.get('username') === 'newuser') + .flush({ displayName: 'New User' }); + + expect(result!.description).toBe('[~New User]'); + + // Verify cache was saved to localStorage + const cachedData = localStorage.getItem('jira.userDisplayNameCache'); + expect(cachedData).toBeTruthy(); + const parsedCache = JSON.parse(cachedData!); + expect(parsedCache['newuser']).toBe('New User'); + + // Clean up + localStorage.removeItem('jira.userDisplayNameCache'); + }); + + it('uses cached user display names from localStorage instead of making API calls', () => { + // Setup localStorage with cached data + const cachedData = { 'cacheduser': 'Cached User' }; + localStorage.setItem('jira.userDisplayNameCache', JSON.stringify(cachedData)); + + // Create a new service instance to load the cache + const newService = TestBed.inject(JiraService); + + let result: JiraTicket | undefined; + newService.getTicketByKey('TEST-1').subscribe((t) => (result = t)); + + httpMock + .expectOne((r) => r.url.includes('/issue/TEST-1')) + .flush(makeRawIssue({ description: '[~cacheduser]' })); + + // Should not make any user API calls since the user is cached + httpMock.expectNone((r) => r.url.includes('/user')); + + expect(result!.description).toBe('[~Cached User]'); + + // Clean up + localStorage.removeItem('jira.userDisplayNameCache'); + }); + + it('handles corrupted localStorage cache gracefully', () => { + // Setup localStorage with invalid data + localStorage.setItem('jira.userDisplayNameCache', 'invalid json data'); + + // Create a new service instance - should not throw + expect(() => { + const newService = TestBed.inject(JiraService); + // Cache should be empty after failed load + expect(newService['userDisplayNameCache'].size).toBe(0); + }).not.toThrow(); + + // Clean up + localStorage.removeItem('jira.userDisplayNameCache'); + }); }); diff --git a/src/app/jira/jira.service.ts b/src/app/jira/jira.service.ts index ef6b680..32766c6 100644 --- a/src/app/jira/jira.service.ts +++ b/src/app/jira/jira.service.ts @@ -119,12 +119,44 @@ export class JiraService { private readonly http = inject(HttpClient); private readonly baseUrl = `${environment.proxyUrl}/jira/rest/api/2`; private readonly userDisplayNameCache = new Map(); + private readonly STORAGE_KEY = 'jira.userDisplayNameCache'; private hasLoadedOnce = false; readonly loading = signal(true); readonly error = signal(false); private readonly _tickets = signal([]); readonly tickets = this._tickets.asReadonly(); + constructor() { + this.loadCacheFromLocalStorage(); + } + + private loadCacheFromLocalStorage(): void { + try { + const cachedData = localStorage.getItem(this.STORAGE_KEY); + if (cachedData) { + const parsed = JSON.parse(cachedData); + if (typeof parsed === 'object' && parsed !== null) { + Object.entries(parsed).forEach(([slug, displayName]) => { + if (typeof slug === 'string' && typeof displayName === 'string') { + this.userDisplayNameCache.set(slug, displayName); + } + }); + } + } + } catch (error) { + console.error('Failed to load user display name cache from localStorage:', error); + } + } + + private saveCacheToLocalStorage(): void { + try { + const cacheObject = Object.fromEntries(this.userDisplayNameCache.entries()); + localStorage.setItem(this.STORAGE_KEY, JSON.stringify(cacheObject)); + } catch (error) { + console.error('Failed to save user display name cache to localStorage:', error); + } + } + loadTickets(): Observable { if (!this.hasLoadedOnce) { this.loading.set(true); @@ -167,9 +199,16 @@ export class JiraService { ), ).pipe( map((results) => { + let cacheUpdated = false; results.forEach((result, i) => { - if (result) this.userDisplayNameCache.set(unknown[i], result.displayName); + if (result) { + this.userDisplayNameCache.set(unknown[i], result.displayName); + cacheUpdated = true; + } }); + if (cacheUpdated) { + this.saveCacheToLocalStorage(); + } }), ); } From 03eae4f50e31c60e85645aecd81b3b0e9b874fc3 Mon Sep 17 00:00:00 2001 From: Dominik Halfkann Date: Sat, 11 Apr 2026 13:03:35 +0200 Subject: [PATCH 2/3] chore: symlink .openhands_instructions to AGENTS.md OpenHands resolver was ignoring coding rules because .openhands_instructions only referenced AGENTS.md indirectly. Symlink ensures the full rules are fed directly to the model. Co-Authored-By: Claude Opus 4.6 (1M context) --- .openhands_instructions | 4 +--- AGENTS.md | 5 +++++ 2 files changed, 6 insertions(+), 3 deletions(-) mode change 100644 => 120000 .openhands_instructions diff --git a/.openhands_instructions b/.openhands_instructions deleted file mode 100644 index b384155..0000000 --- a/.openhands_instructions +++ /dev/null @@ -1,3 +0,0 @@ -Read and follow `/AGENTS.md` — it is the single source of truth for project rules, coding standards, and design constraints. - -Do not commit documentation files, standalone test scripts, or debugging artifacts. Only commit production code and its corresponding spec files. diff --git a/.openhands_instructions b/.openhands_instructions new file mode 120000 index 0000000..47dc3e3 --- /dev/null +++ b/.openhands_instructions @@ -0,0 +1 @@ +AGENTS.md \ No newline at end of file diff --git a/AGENTS.md b/AGENTS.md index 3ecbe18..8d1d682 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -164,6 +164,11 @@ Every card (ticket, PR, todo, idea) has exactly one state: Cards never have colored backgrounds. Color lives only in badges, icons, text, and the amber attention bar. Type badges ("Fehler", "Aufgabe", "User Story") are always neutral stone — only status badges carry semantic colors. +## Commit Hygiene + +- Do not commit documentation files, standalone test scripts, or debugging artifacts +- Only commit production code and its corresponding spec files + ## Accessibility - Must pass all AXE checks From 712c9400df3f4b1ba7efebe1f9faa5a40015e746 Mon Sep 17 00:00:00 2001 From: Dominik Halfkann Date: Sat, 11 Apr 2026 13:05:24 +0200 Subject: [PATCH 3/3] =?UTF-8?q?fix:=20clean=20up=20PR=20=E2=80=94=20fix=20?= =?UTF-8?q?failing=20tests,=20remove=20comments,=20drop=20unrelated=20chan?= =?UTF-8?q?ge?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Revert unrelated angular.json analytics change - Remove explanatory comments per AGENTS.md - Fix cache tests by using TestBed.resetTestingModule() to create fresh service instances that actually load from localStorage Co-Authored-By: Claude Opus 4.6 (1M context) --- angular.json | 3 +- src/app/jira/jira.service.spec.ts | 76 +++++++++++++------------------ 2 files changed, 32 insertions(+), 47 deletions(-) diff --git a/angular.json b/angular.json index c65c792..9eee768 100644 --- a/angular.json +++ b/angular.json @@ -2,8 +2,7 @@ "$schema": "./node_modules/@angular/cli/lib/config/schema.json", "version": 1, "cli": { - "packageManager": "npm", - "analytics": false + "packageManager": "npm" }, "newProjectRoot": "projects", "projects": { diff --git a/src/app/jira/jira.service.spec.ts b/src/app/jira/jira.service.spec.ts index 8b4b548..f7dfc94 100644 --- a/src/app/jira/jira.service.spec.ts +++ b/src/app/jira/jira.service.spec.ts @@ -9,7 +9,6 @@ describe('JiraService', () => { let httpMock: HttpTestingController; beforeEach(() => { - // Clear localStorage before each test to ensure clean state localStorage.removeItem('jira.userDisplayNameCache'); TestBed.configureTestingModule({ @@ -374,29 +373,24 @@ describe('JiraService', () => { }); it('loads user display name cache from localStorage on service initialization', () => { - // Clear localStorage first - localStorage.removeItem('jira.userDisplayNameCache'); - - // Setup localStorage with cached data - const cachedData = { 'u1': 'User One', 'u2': 'User Two' }; - localStorage.setItem('jira.userDisplayNameCache', JSON.stringify(cachedData)); + localStorage.setItem( + 'jira.userDisplayNameCache', + JSON.stringify({ u1: 'User One', u2: 'User Two' }), + ); - // Create a new service instance - const newService = TestBed.inject(JiraService); - - // Verify cache was loaded - expect(newService['userDisplayNameCache'].size).toBe(2); - expect(newService['userDisplayNameCache'].get('u1')).toBe('User One'); - expect(newService['userDisplayNameCache'].get('u2')).toBe('User Two'); + TestBed.resetTestingModule(); + TestBed.configureTestingModule({ + providers: [provideHttpClient(withFetch()), provideHttpClientTesting()], + }); + const freshService = TestBed.inject(JiraService); + httpMock = TestBed.inject(HttpTestingController); - // Clean up - localStorage.removeItem('jira.userDisplayNameCache'); + expect(freshService['userDisplayNameCache'].size).toBe(2); + expect(freshService['userDisplayNameCache'].get('u1')).toBe('User One'); + expect(freshService['userDisplayNameCache'].get('u2')).toBe('User Two'); }); it('saves resolved user display names to localStorage', () => { - // Clear localStorage first - localStorage.removeItem('jira.userDisplayNameCache'); - let result: JiraTicket | undefined; service.getTicketByKey('TEST-1').subscribe((t) => (result = t)); @@ -409,52 +403,44 @@ describe('JiraService', () => { expect(result!.description).toBe('[~New User]'); - // Verify cache was saved to localStorage - const cachedData = localStorage.getItem('jira.userDisplayNameCache'); - expect(cachedData).toBeTruthy(); - const parsedCache = JSON.parse(cachedData!); - expect(parsedCache['newuser']).toBe('New User'); - - // Clean up - localStorage.removeItem('jira.userDisplayNameCache'); + const parsed = JSON.parse(localStorage.getItem('jira.userDisplayNameCache')!); + expect(parsed['newuser']).toBe('New User'); }); it('uses cached user display names from localStorage instead of making API calls', () => { - // Setup localStorage with cached data - const cachedData = { 'cacheduser': 'Cached User' }; - localStorage.setItem('jira.userDisplayNameCache', JSON.stringify(cachedData)); + localStorage.setItem('jira.userDisplayNameCache', JSON.stringify({ cacheduser: 'Cached User' })); - // Create a new service instance to load the cache - const newService = TestBed.inject(JiraService); + TestBed.resetTestingModule(); + TestBed.configureTestingModule({ + providers: [provideHttpClient(withFetch()), provideHttpClientTesting()], + }); + const freshService = TestBed.inject(JiraService); + httpMock = TestBed.inject(HttpTestingController); let result: JiraTicket | undefined; - newService.getTicketByKey('TEST-1').subscribe((t) => (result = t)); + freshService.getTicketByKey('TEST-1').subscribe((t) => (result = t)); httpMock .expectOne((r) => r.url.includes('/issue/TEST-1')) .flush(makeRawIssue({ description: '[~cacheduser]' })); - // Should not make any user API calls since the user is cached httpMock.expectNone((r) => r.url.includes('/user')); - expect(result!.description).toBe('[~Cached User]'); - - // Clean up - localStorage.removeItem('jira.userDisplayNameCache'); }); it('handles corrupted localStorage cache gracefully', () => { - // Setup localStorage with invalid data localStorage.setItem('jira.userDisplayNameCache', 'invalid json data'); - // Create a new service instance - should not throw + TestBed.resetTestingModule(); + TestBed.configureTestingModule({ + providers: [provideHttpClient(withFetch()), provideHttpClientTesting()], + }); + expect(() => { - const newService = TestBed.inject(JiraService); - // Cache should be empty after failed load - expect(newService['userDisplayNameCache'].size).toBe(0); + const freshService = TestBed.inject(JiraService); + expect(freshService['userDisplayNameCache'].size).toBe(0); }).not.toThrow(); - // Clean up - localStorage.removeItem('jira.userDisplayNameCache'); + httpMock = TestBed.inject(HttpTestingController); }); });