1
0
mirror of https://github.com/sasjs/server.git synced 2026-07-23 21:25:29 +00:00

fix(api): return prompt error response instead of hanging when a SAS session fails

The SAS-runtime poll loop in processProgram only checked for
SessionState.completed, so a session that failed (e.g. via %abort;)
left the loop spinning on delay(50) forever - the request never
resolved, even though the SAS log had already been written. Split a single completed-on-either-outcome
flag into separate completed/failed states without updating this loop.

- processProgram now throws when session.state becomes 'failed'
- Execution.ts catches that, reads the complete log (guaranteed
  complete since the session's process has already exited by then),
  and throws a SessionExecutionError carrying it
- stp.ts/code.ts surface { ..., log } in the HTTP error response
- add unit tests for the poll loop and the log-enrichment logic, plus
  a mock SAS executable + end-to-end test exercising the real
  session/process pipeline without needing a SAS install
- add docs/diagrams covering the session lifecycle and the SAS
  execution handshake mechanism, for future context
This commit is contained in:
YuryShkoda
2026-07-13 13:46:36 +03:00
parent 8b2a6155a9
commit f84af4ac06
12 changed files with 658 additions and 14 deletions
@@ -0,0 +1,65 @@
import path from 'path'
import os from 'os'
import { createFile, deleteFolder, generateTimestamp } from '@sasjs/utils'
import * as ProcessProgramModule from '../processProgram'
import { ExecutionController, SessionExecutionError } from '../Execution'
import { Session, SessionState, PreProgramVars } from '../../../types'
import { RunTimeType } from '../../../utils'
const preProgramVariables: PreProgramVars = {
username: 'testUser',
userId: 1,
displayName: 'Test User',
serverUrl: 'http://localhost:5000',
httpHeaders: []
}
describe('ExecutionController.executeProgram (SAS failure path)', () => {
let session: Session
beforeEach(() => {
const sessionId = `test-session-${generateTimestamp()}`
session = {
id: sessionId,
state: SessionState.pending,
creationTimeStamp: '0',
deathTimeStamp: '0',
path: path.join(os.tmpdir(), sessionId)
}
})
afterEach(async () => {
jest.restoreAllMocks()
await deleteFolder(session.path)
})
it('throws a SessionExecutionError carrying the complete log when the session fails', async () => {
const logPath = path.join(session.path, 'log.log')
const logContent =
'NOTE: SAS session\nERROR: SAS session terminated. See log for details.\n'
await createFile(logPath, logContent)
jest
.spyOn(ProcessProgramModule, 'processProgram')
.mockImplementation(async () => {
throw new Error('ERROR: SAS session terminated. See log for details.')
})
const controller = new ExecutionController()
const resultPromise = controller.executeProgram({
program: '%abort;',
preProgramVariables,
vars: {},
session,
runTime: RunTimeType.SAS
})
await expect(resultPromise).rejects.toBeInstanceOf(SessionExecutionError)
await expect(resultPromise).rejects.toMatchObject({
log: logContent,
message: expect.stringContaining('SAS session terminated')
})
})
})