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
16 changes: 12 additions & 4 deletions apps/frontend-docs/src/i18n/messages.de.xlf
Original file line number Diff line number Diff line change
Expand Up @@ -55,17 +55,25 @@
<target>Laden...</target>
</trans-unit>
<trans-unit id="featureDocsPage-metaTitleFallback" datatype="html">
<source>Centralized Control for Distributed AI Agent Infrastructure</source>
<target>Zentrale Steuerung für verteilte KI-Agent-Infrastruktur</target>
<source>Documentation :: Agenstra</source>
<target>Dokumentation :: Agenstra</target>
</trans-unit>
<trans-unit id="featureDocsPage-metaDescriptionFallback" datatype="html">
<source>Centralized Control for Distributed AI Agent Infrastructure</source>
<target>Zentrale Steuerung für verteilte KI-Agent-Infrastruktur</target>
<source>Official Agenstra documentation: install, deploy, secure, and operate agent hosts, workspaces, tickets, APIs, and integrations for platform teams.</source>
<target>Offizielle Agenstra-Dokumentation: Installation, Deployment, Sicherheit und Betrieb von Agent-Hosts, Workspaces, Tickets, APIs und Integrationen für Plattformteams.</target>
</trans-unit>
<trans-unit id="featureDocsPage-metaKeywords" datatype="html">
<source>Agenstra, AI agents, agent management, distributed systems, AI agent infrastructure, agent platform, AI agent console, container management, WebSocket agents, Docker agents</source>
<target>Agenstra, KI-Agents, Agent-Verwaltung, verteilte Systeme, KI-Agent-Infrastruktur, Agent-Plattform, KI-Agent-Konsole, Container-Verwaltung, WebSocket-Agents, Docker-Agents</target>
</trans-unit>
<trans-unit id="featureDocsSearchPage-metaTitle" datatype="html">
<source>Search Documentation :: Agenstra</source>
<target>Dokumentation durchsuchen :: Agenstra</target>
</trans-unit>
<trans-unit id="featureDocsSearchPage-metaDescription" datatype="html">
<source>Search Agenstra docs for setup guides, API references, security hardening, agent configuration, deployment patterns, and troubleshooting.</source>
<target>Durchsuchen Sie die Agenstra-Dokumentation nach Setup-Anleitungen, API-Referenzen, Security-Hardening, Agent-Konfiguration, Deployment-Mustern und Troubleshooting.</target>
</trans-unit>
<trans-unit id="featureDocsSearchPage-title" datatype="html">
<source>Search Documentation</source>
<target>Dokumentation durchsuchen</target>
Expand Down
10 changes: 8 additions & 2 deletions apps/frontend-docs/src/i18n/messages.xlf
Original file line number Diff line number Diff line change
Expand Up @@ -42,14 +42,20 @@
<source>Loading...</source>
</trans-unit>
<trans-unit id="featureDocsPage-metaTitleFallback" datatype="html">
<source>Centralized Control for Distributed AI Agent Infrastructure</source>
<source>Documentation :: Agenstra</source>
</trans-unit>
<trans-unit id="featureDocsPage-metaDescriptionFallback" datatype="html">
<source>Centralized Control for Distributed AI Agent Infrastructure</source>
<source>Official Agenstra documentation: install, deploy, secure, and operate agent hosts, workspaces, tickets, APIs, and integrations for platform teams.</source>
</trans-unit>
<trans-unit id="featureDocsPage-metaKeywords" datatype="html">
<source>Agenstra, AI agents, agent management, distributed systems, AI agent infrastructure, agent platform, AI agent console, container management, WebSocket agents, Docker agents</source>
</trans-unit>
<trans-unit id="featureDocsSearchPage-metaTitle" datatype="html">
<source>Search Documentation :: Agenstra</source>
</trans-unit>
<trans-unit id="featureDocsSearchPage-metaDescription" datatype="html">
<source>Search Agenstra docs for setup guides, API references, security hardening, agent configuration, deployment patterns, and troubleshooting.</source>
</trans-unit>
<trans-unit id="featureDocsSearchPage-title" datatype="html">
<source>Search Documentation</source>
</trans-unit>
Expand Down
2,619 changes: 1,436 additions & 1,183 deletions apps/frontend-portal/src/i18n/messages.de.xlf

Large diffs are not rendered by default.

1,063 changes: 631 additions & 432 deletions apps/frontend-portal/src/i18n/messages.xlf

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { Actions } from '@ngrx/effects';
import { provideMockActions } from '@ngrx/effects/testing';
import { of, throwError } from 'rxjs';

import { SERVICE_PLANS_BATCH_SIZE } from '../../constants/service-plans.constants';
import { PublicServicePlanOfferingsService } from '../../services/public-service-plan-offerings.service';
import type { PublicServicePlanOffering } from '../../types/portal-service-plans.types';

Expand Down Expand Up @@ -31,6 +32,12 @@ describe('Portal ServicePlansEffects', () => {
totalPrice: 99,
orderingHighlights: [],
};
const createOfferings = (count: number): PublicServicePlanOffering[] =>
Array.from({ length: count }, (_, index) => ({
...mockOffering,
id: `sp-${index}`,
name: `Plan ${index}`,
}));

beforeEach(() => {
offeringsService = {
Expand Down Expand Up @@ -59,12 +66,50 @@ describe('Portal ServicePlansEffects', () => {
});
});

it('should return loadServicePlansFailure on error', (done) => {
it('should return loadServicePlansSuccess when batch is smaller than page size', (done) => {
const partialBatch = createOfferings(3);

actions$ = of(loadServicePlans({ params: {} }));
offeringsService.listOfferings.mockReturnValue(of(partialBatch));

loadServicePlans$(actions$, offeringsService).subscribe((result) => {
expect(result).toEqual(loadServicePlansSuccess({ servicePlans: partialBatch }));
done();
});
});

it('should return loadServicePlansBatch when first page is full', (done) => {
const fullBatch = createOfferings(SERVICE_PLANS_BATCH_SIZE);

actions$ = of(loadServicePlans({ params: { limit: 25 } }));
offeringsService.listOfferings.mockReturnValue(of(fullBatch));

loadServicePlans$(actions$, offeringsService).subscribe((result) => {
expect(result).toEqual(
loadServicePlansBatch({
offset: SERVICE_PLANS_BATCH_SIZE,
accumulatedServicePlans: fullBatch,
}),
);
expect(offeringsService.listOfferings).toHaveBeenCalledWith({
limit: 25,
offset: 0,
});
done();
});
});

it.each([
['Error', () => new Error('Load failed'), 'Load failed'],
['string', () => 'Load failed', 'Load failed'],
['object message', () => ({ message: 'Load failed' }), 'Load failed'],
['unknown', () => ({ code: 500 }), 'An unexpected error occurred'],
])('should return loadServicePlansFailure on %s', (_label, errorFactory, expectedMessage, done) => {
actions$ = of(loadServicePlans({ params: {} }));
offeringsService.listOfferings.mockReturnValue(throwError(() => new Error('Load failed')));
offeringsService.listOfferings.mockReturnValue(throwError(errorFactory));

loadServicePlans$(actions$, offeringsService).subscribe((result) => {
expect(result).toEqual(loadServicePlansFailure({ error: 'Load failed' }));
expect(result).toEqual(loadServicePlansFailure({ error: expectedMessage }));
done();
});
});
Expand All @@ -82,6 +127,52 @@ describe('Portal ServicePlansEffects', () => {
done();
});
});

it('should return loadServicePlansSuccess when follow-up batch is partial', (done) => {
const accumulated = createOfferings(SERVICE_PLANS_BATCH_SIZE);
const followUp = createOfferings(2);

actions$ = of(loadServicePlansBatch({ offset: SERVICE_PLANS_BATCH_SIZE, accumulatedServicePlans: accumulated }));
offeringsService.listOfferings.mockReturnValue(of(followUp));

loadServicePlansBatch$(actions$, offeringsService).subscribe((result) => {
expect(result).toEqual(loadServicePlansSuccess({ servicePlans: [...accumulated, ...followUp] }));
done();
});
});

it('should return loadServicePlansBatch when follow-up batch is full', (done) => {
const accumulated = createOfferings(SERVICE_PLANS_BATCH_SIZE);
const followUp = createOfferings(SERVICE_PLANS_BATCH_SIZE);

actions$ = of(loadServicePlansBatch({ offset: SERVICE_PLANS_BATCH_SIZE, accumulatedServicePlans: accumulated }));
offeringsService.listOfferings.mockReturnValue(of(followUp));

loadServicePlansBatch$(actions$, offeringsService).subscribe((result) => {
expect(result).toEqual(
loadServicePlansBatch({
offset: SERVICE_PLANS_BATCH_SIZE * 2,
accumulatedServicePlans: [...accumulated, ...followUp],
}),
);
done();
});
});

it.each([
['Error', () => new Error('Batch failed'), 'Batch failed'],
['string', () => 'Batch failed', 'Batch failed'],
['object message', () => ({ message: 'Batch failed' }), 'Batch failed'],
['unknown', () => null, 'An unexpected error occurred'],
])('should return loadServicePlansFailure on %s', (_label, errorFactory, expectedMessage, done) => {
actions$ = of(loadServicePlansBatch({ offset: 10, accumulatedServicePlans: [mockOffering] }));
offeringsService.listOfferings.mockReturnValue(throwError(errorFactory));

loadServicePlansBatch$(actions$, offeringsService).subscribe((result) => {
expect(result).toEqual(loadServicePlansFailure({ error: expectedMessage }));
done();
});
});
});

describe('loadCheapestServicePlanOffering$', () => {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { TestBed } from '@angular/core/testing';
import { Store } from '@ngrx/store';
import { of } from 'rxjs';
import { firstValueFrom, of } from 'rxjs';

import type { PublicServicePlanOffering } from '../../types/portal-service-plans.types';

Expand Down Expand Up @@ -32,21 +32,41 @@ describe('ServicePlansFacade', () => {
facade = TestBed.inject(ServicePlansFacade);
});

it('getCheapestServicePlanOffering$ should select cheapest offering', (done) => {
store.select.mockReturnValue(of(mockOffering));
facade.getCheapestServicePlanOffering$().subscribe((result) => {
expect(result).toEqual(mockOffering);
done();
});
it.each([
['getServicePlans$', () => facade.getServicePlans$(), [mockOffering]],
['getCheapestServicePlanOffering$', () => facade.getCheapestServicePlanOffering$(), mockOffering],
['getServicePlansLoading$', () => facade.getServicePlansLoading$(), true],
['getCheapestServicePlanOfferingLoading$', () => facade.getCheapestServicePlanOfferingLoading$(), true],
['getServicePlansLoaded$', () => facade.getServicePlansLoaded$(), true],
['getCheapestServicePlanOfferingLoaded$', () => facade.getCheapestServicePlanOfferingLoaded$(), true],
['getServicePlansError$', () => facade.getServicePlansError$(), 'load failed'],
['getCheapestServicePlanOfferingError$', () => facade.getCheapestServicePlanOfferingError$(), 'not found'],
['getServicePlansCount$', () => facade.getServicePlansCount$(), 2],
['hasServicePlans$', () => facade.hasServicePlans$(), true],
['getServicePlanById$', () => facade.getServicePlanById$('sp-1'), mockOffering],
['getServicePlansByServiceTypeId$', () => facade.getServicePlansByServiceTypeId$('st-1'), [mockOffering]],
])('%s should select from store', async (_label, observableFactory, expected) => {
store.select.mockReturnValue(of(expected));
await expect(firstValueFrom(observableFactory())).resolves.toEqual(expected);
});

it('loadCheapestServicePlanOffering should dispatch', () => {
it('loadCheapestServicePlanOffering should dispatch with service type', () => {
facade.loadCheapestServicePlanOffering('st-x');
expect(store.dispatch).toHaveBeenCalledWith(loadCheapestServicePlanOffering({ serviceTypeId: 'st-x' }));
});

it('loadServicePlans should dispatch', () => {
it('loadCheapestServicePlanOffering should dispatch without service type', () => {
facade.loadCheapestServicePlanOffering();
expect(store.dispatch).toHaveBeenCalledWith(loadCheapestServicePlanOffering({ serviceTypeId: undefined }));
});

it('loadServicePlans should dispatch with params', () => {
facade.loadServicePlans({ limit: 5 });
expect(store.dispatch).toHaveBeenCalledWith(loadServicePlans({ params: { limit: 5 } }));
});

it('loadServicePlans should dispatch without params', () => {
facade.loadServicePlans();
expect(store.dispatch).toHaveBeenCalledWith(loadServicePlans({ params: undefined }));
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,16 @@ import type {
import { loadCheapestServicePlanOffering, loadServicePlans } from './service-plans.actions';
import {
selectCheapestServicePlanOffering,
selectCheapestServicePlanOfferingError,
selectCheapestServicePlanOfferingLoaded,
selectCheapestServicePlanOfferingLoading,
selectHasServicePlans,
selectServicePlansByServiceTypeId,
selectServicePlanById,
selectServicePlansCount,
selectServicePlansEntities,
selectServicePlansError,
selectServicePlansLoaded,
selectServicePlansLoading,
} from './service-plans.selectors';

Expand All @@ -42,10 +45,22 @@ export class ServicePlansFacade {
return this.store.select(selectCheapestServicePlanOfferingLoading);
}

getServicePlansLoaded$(): Observable<boolean> {
return this.store.select(selectServicePlansLoaded);
}

getCheapestServicePlanOfferingLoaded$(): Observable<boolean> {
return this.store.select(selectCheapestServicePlanOfferingLoaded);
}

getServicePlansError$(): Observable<string | null> {
return this.store.select(selectServicePlansError);
}

getCheapestServicePlanOfferingError$(): Observable<string | null> {
return this.store.select(selectCheapestServicePlanOfferingError);
}

getServicePlansCount$(): Observable<number> {
return this.store.select(selectServicePlansCount);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ describe('servicePlansReducer', () => {
});

describe('loadServicePlans', () => {
it('should set loading and clear entities', () => {
it('should set loading and keep cached entities', () => {
const state: ServicePlansState = {
...initialServicePlansState,
entities: [mockOffering],
Expand All @@ -38,8 +38,8 @@ describe('servicePlansReducer', () => {
const newState = servicePlansReducer(state, loadServicePlans({ params: {} }));

expect(newState.loading).toBe(true);
expect(newState.entities).toEqual([]);
expect(newState.error).toBeNull();
expect(newState.entities).toEqual([mockOffering]);
expect(newState.plansError).toBeNull();
});
});

Expand All @@ -65,19 +65,21 @@ describe('servicePlansReducer', () => {

expect(newState.entities).toEqual([mockOffering]);
expect(newState.loading).toBe(false);
expect(newState.error).toBeNull();
expect(newState.plansLoaded).toBe(true);
expect(newState.plansError).toBeNull();
});
});

describe('loadServicePlansFailure', () => {
it('should set error and clear loading', () => {
it('should set plansError and mark plans as loaded', () => {
const newState = servicePlansReducer(
{ ...initialServicePlansState, loading: true },
loadServicePlansFailure({ error: 'failed' }),
);

expect(newState.loading).toBe(false);
expect(newState.error).toBe('failed');
expect(newState.plansLoaded).toBe(true);
expect(newState.plansError).toBe('failed');
});
});

Expand All @@ -86,7 +88,7 @@ describe('servicePlansReducer', () => {
const newState = servicePlansReducer(initialServicePlansState, loadCheapestServicePlanOffering({}));

expect(newState.loadingCheapest).toBe(true);
expect(newState.error).toBeNull();
expect(newState.cheapestError).toBeNull();
});
});

Expand All @@ -99,18 +101,20 @@ describe('servicePlansReducer', () => {

expect(newState.cheapestOffering).toEqual(mockOffering);
expect(newState.loadingCheapest).toBe(false);
expect(newState.cheapestLoaded).toBe(true);
});
});

describe('loadCheapestServicePlanOfferingFailure', () => {
it('should set error and clear loadingCheapest', () => {
it('should set cheapestError and mark cheapest as loaded', () => {
const newState = servicePlansReducer(
{ ...initialServicePlansState, loadingCheapest: true },
loadCheapestServicePlanOfferingFailure({ error: 'not found' }),
);

expect(newState.loadingCheapest).toBe(false);
expect(newState.error).toBe('not found');
expect(newState.cheapestLoaded).toBe(true);
expect(newState.cheapestError).toBe('not found');
});
});
});
Loading
Loading