When the startupservice fails, the modal is a dead end: the adapter returned no log, so there is nothing to act on. The modal now offers a button that runs the same service through the JES / STP web app in a new browser window, where the SAS log is returned as the page body. `_debug=131` is always added (the log is the point), and `_executionTasks` is never included, so on Viya the run is not submitted as a Compute task and cannot hit the execution-task timeout. The URL is built by `buildServiceUrl`, which works for SAS 9, Viya and SASjs Server, and is the same shape the deploy configurator already uses to open makedata. The startup failure modals now carry the service name, which is what shows the button: all three failure branches (malformed response, missing properties, request rejected) pass `public/startupservice`.
181 lines
6.1 KiB
TypeScript
181 lines
6.1 KiB
TypeScript
import { EventEmitter } from '@angular/core'
|
|
import { BehaviorSubject, Subject } from 'rxjs'
|
|
import { AppService } from './app.service'
|
|
|
|
/**
|
|
* AppService's constructor subscribes to several emitters/subjects (see
|
|
* subscribe() and the persistSelectedTheme check) and startUpData() touches
|
|
* a handful of methods on each collaborator — these stubs cover exactly
|
|
* that surface, not the full real services (no TestBed/DI needed, same
|
|
* plain-instantiation-with-stubs precedent as va-filter.service.spec.ts).
|
|
*/
|
|
const buildDeps = () => {
|
|
const licenceService: any = {
|
|
isAppActivated: new BehaviorSubject<boolean | null>(null),
|
|
activation: jasmine.createSpy('activation').and.resolveTo(undefined)
|
|
}
|
|
const eventService: any = {
|
|
showInfoModal: jasmine.createSpy('showInfoModal'),
|
|
startupDataLoaded: jasmine.createSpy('startupDataLoaded'),
|
|
toggleDarkMode: jasmine.createSpy('toggleDarkMode')
|
|
}
|
|
const sasService: any = {
|
|
loadStartupServiceEmitter: new EventEmitter<any>(),
|
|
requestSiteIdEmitter: new EventEmitter<string>(),
|
|
request: jasmine.createSpy('request')
|
|
}
|
|
const loggerService: any = {
|
|
log: jasmine.createSpy('log')
|
|
}
|
|
const appSettingsService: any = {
|
|
settings: new BehaviorSubject({ persistSelectedTheme: false } as any)
|
|
}
|
|
const router: any = {
|
|
events: new Subject(),
|
|
url: '',
|
|
navigateByUrl: jasmine.createSpy('navigateByUrl')
|
|
}
|
|
const appStoreService: any = {
|
|
getDcAdapterSettings: () => undefined
|
|
}
|
|
const startupCheckService: any = {
|
|
updateStep: jasmine.createSpy('updateStep')
|
|
}
|
|
|
|
return {
|
|
licenceService,
|
|
eventService,
|
|
sasService,
|
|
loggerService,
|
|
appSettingsService,
|
|
router,
|
|
appStoreService,
|
|
startupCheckService
|
|
}
|
|
}
|
|
|
|
const buildAppService = (deps: ReturnType<typeof buildDeps>) =>
|
|
new AppService(
|
|
deps.licenceService,
|
|
deps.eventService,
|
|
deps.sasService,
|
|
deps.loggerService,
|
|
deps.appSettingsService,
|
|
deps.router,
|
|
deps.appStoreService,
|
|
deps.startupCheckService
|
|
)
|
|
|
|
// Minimal valid startupservice payload — just enough to pass startUpData()'s
|
|
// missing-props check (Globvars/Sasdatasets/Saslibs/XLMaps).
|
|
const validStartupResponse = () => ({
|
|
adapterResponse: {
|
|
SYSSITE: 'SITE1',
|
|
globvars: [{ ISADMIN: false, DC_ADMIN_GROUP: '', DCLIB: 'DCLIB' }],
|
|
sasdatasets: [],
|
|
saslibs: {},
|
|
xlmaps: []
|
|
}
|
|
})
|
|
|
|
describe('AppService - startup retry', () => {
|
|
it('retries once and succeeds without showing the error modal when only the first attempt rejects', async () => {
|
|
const deps = buildDeps()
|
|
deps.sasService.request.and.returnValues(
|
|
Promise.reject('timeout'),
|
|
Promise.resolve(validStartupResponse())
|
|
)
|
|
|
|
const appService = buildAppService(deps)
|
|
// Keep the retry pause instant in tests — see STARTUP_RETRY_PLAN.md.
|
|
;(appService as any).retryOptions = {
|
|
wait: () => Promise.resolve(),
|
|
random: () => 0
|
|
}
|
|
|
|
await appService.startUpData()
|
|
|
|
expect(deps.sasService.request).toHaveBeenCalledTimes(2)
|
|
expect(deps.eventService.showInfoModal).not.toHaveBeenCalled()
|
|
expect(deps.eventService.startupDataLoaded).toHaveBeenCalled()
|
|
})
|
|
|
|
it('shows the error modal once (not twice) when both attempts reject', async () => {
|
|
const deps = buildDeps()
|
|
deps.sasService.request.and.returnValues(
|
|
Promise.reject('timeout-1'),
|
|
Promise.reject('timeout-2')
|
|
)
|
|
|
|
const appService = buildAppService(deps)
|
|
;(appService as any).retryOptions = {
|
|
wait: () => Promise.resolve(),
|
|
random: () => 0
|
|
}
|
|
|
|
await appService.startUpData()
|
|
|
|
expect(deps.sasService.request).toHaveBeenCalledTimes(2)
|
|
expect(deps.eventService.showInfoModal).toHaveBeenCalledTimes(1)
|
|
expect(deps.licenceService.isAppActivated.value).toBeFalse()
|
|
})
|
|
|
|
it('does not retry a response that arrives but is missing required properties', async () => {
|
|
const deps = buildDeps()
|
|
deps.sasService.request.and.resolveTo({
|
|
adapterResponse: {
|
|
SYSSITE: 'SITE1',
|
|
// globvars deliberately omitted
|
|
sasdatasets: [],
|
|
saslibs: {},
|
|
xlmaps: []
|
|
}
|
|
})
|
|
|
|
const appService = buildAppService(deps)
|
|
;(appService as any).retryOptions = {
|
|
wait: () => Promise.resolve(),
|
|
random: () => 0
|
|
}
|
|
|
|
await appService.startUpData()
|
|
|
|
expect(deps.sasService.request).toHaveBeenCalledTimes(1)
|
|
expect(deps.eventService.showInfoModal).toHaveBeenCalledTimes(1)
|
|
})
|
|
|
|
// Reproduces a misconfigured Viya computeTasks deployment: the Compute
|
|
// service returns a plain-text "Job error" body instead of JSON: the
|
|
// adapter can't parse it and resolves with the raw text as adapterResponse
|
|
// itself (see getMalformedAdapterResponseMessage's own doc comment).
|
|
// "Globvars, Sasdatasets, Saslibs, XLMaps are not present" would also be
|
|
// technically true here, but hides the real cause.
|
|
it('shows the real response text (not the generic missing-props message) when adapterResponse is a raw string, not an object', async () => {
|
|
const deps = buildDeps()
|
|
const rawJobError = [
|
|
'Job error',
|
|
'The Compute service could not execute the task c74a707a-8b88-46a5-8f28-9a7694ad5e13 because the context 340cd3eb-72ae is not reusable. Please provide a reusable context or supply the ID of an existing session in the task request.',
|
|
'path: /compute/tasks',
|
|
'correlator: dc1a9fda-8f65-4f32-b4d4-2a70e02179b1;1bcb6a4c-8a16-4053-a790-292544c25a03'
|
|
].join('\n')
|
|
deps.sasService.request.and.resolveTo({ adapterResponse: rawJobError })
|
|
|
|
const appService = buildAppService(deps)
|
|
;(appService as any).retryOptions = {
|
|
wait: () => Promise.resolve(),
|
|
random: () => 0
|
|
}
|
|
|
|
await appService.startUpData()
|
|
|
|
expect(deps.sasService.request).toHaveBeenCalledTimes(1)
|
|
expect(deps.eventService.showInfoModal).toHaveBeenCalledTimes(1)
|
|
const [, message, sasService] =
|
|
deps.eventService.showInfoModal.calls.mostRecent().args
|
|
expect(message).toContain(rawJobError)
|
|
expect(message).not.toContain('Globvars, Sasdatasets, Saslibs, XLMaps')
|
|
expect(sasService).toBe('public/startupservice')
|
|
expect(deps.licenceService.isAppActivated.value).toBeFalse()
|
|
})
|
|
})
|