Skip to content

Commit 02c6cc3

Browse files
authored
Merge pull request #150 from actions/sgoedecke/mock-inference-in-ci
Mock inference in CI
2 parents 5022b33 + 18d4686 commit 02c6cc3

File tree

7 files changed

+158
-66
lines changed

7 files changed

+158
-66
lines changed

.github/workflows/ci.yml

Lines changed: 71 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -56,20 +56,51 @@ jobs:
5656
id: checkout
5757
uses: actions/checkout@v5
5858

59+
- name: Setup Node.js
60+
uses: actions/setup-node@v4
61+
with:
62+
node-version-file: .node-version
63+
64+
- name: Start Mock Inference Server
65+
id: mock-server
66+
run: |
67+
node script/mock-inference-server.mjs &
68+
echo "pid=$!" >> $GITHUB_OUTPUT
69+
# Wait for server to be ready
70+
for i in {1..10}; do
71+
if curl -s http://localhost:3456/health > /dev/null; then
72+
echo "Mock server is ready"
73+
break
74+
fi
75+
sleep 1
76+
done
77+
5978
- name: Test Local Action
6079
id: test-action
61-
continue-on-error: true
6280
uses: ./
6381
with:
6482
prompt: hello
83+
endpoint: http://localhost:3456
6584
env:
6685
GITHUB_TOKEN: ${{ github.token }}
6786

6887
- name: Print Output
6988
id: output
70-
continue-on-error: true
7189
run: echo "${{ steps.test-action.outputs.response }}"
7290

91+
- name: Verify Output
92+
run: |
93+
response="${{ steps.test-action.outputs.response }}"
94+
if [[ -z "$response" ]]; then
95+
echo "Error: No response received"
96+
exit 1
97+
fi
98+
echo "Response received: $response"
99+
100+
- name: Stop Mock Server
101+
if: always()
102+
run: kill ${{ steps.mock-server.outputs.pid }} || true
103+
73104
test-action-prompt-file:
74105
name: GitHub Actions Test with Prompt File
75106
runs-on: ubuntu-latest
@@ -79,6 +110,25 @@ jobs:
79110
id: checkout
80111
uses: actions/checkout@v5
81112

113+
- name: Setup Node.js
114+
uses: actions/setup-node@v4
115+
with:
116+
node-version-file: .node-version
117+
118+
- name: Start Mock Inference Server
119+
id: mock-server
120+
run: |
121+
node script/mock-inference-server.mjs &
122+
echo "pid=$!" >> $GITHUB_OUTPUT
123+
# Wait for server to be ready
124+
for i in {1..10}; do
125+
if curl -s http://localhost:3456/health > /dev/null; then
126+
echo "Mock server is ready"
127+
break
128+
fi
129+
sleep 1
130+
done
131+
82132
- name: Create Prompt File
83133
run: echo "hello" > prompt.txt
84134

@@ -87,16 +137,33 @@ jobs:
87137

88138
- name: Test Local Action with Prompt File
89139
id: test-action-prompt-file
90-
continue-on-error: true
91140
uses: ./
92141
with:
93142
prompt-file: prompt.txt
94143
system-prompt-file: system-prompt.txt
144+
endpoint: http://localhost:3456
95145
env:
96146
GITHUB_TOKEN: ${{ github.token }}
97147

98148
- name: Print Output
99-
continue-on-error: true
100149
run: |
101150
echo "Response saved to: ${{ steps.test-action-prompt-file.outputs.response-file }}"
102151
cat "${{ steps.test-action-prompt-file.outputs.response-file }}"
152+
153+
- name: Verify Output
154+
run: |
155+
response_file="${{ steps.test-action-prompt-file.outputs.response-file }}"
156+
if [[ ! -f "$response_file" ]]; then
157+
echo "Error: Response file not found"
158+
exit 1
159+
fi
160+
content=$(cat "$response_file")
161+
if [[ -z "$content" ]]; then
162+
echo "Error: Response file is empty"
163+
exit 1
164+
fi
165+
echo "Response file content: $content"
166+
167+
- name: Stop Mock Server
168+
if: always()
169+
run: kill ${{ steps.mock-server.outputs.pid }} || true

__tests__/main.test.ts

Lines changed: 4 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -75,17 +75,13 @@ vi.mock('fs', () => ({
7575
writeFileSync: mockWriteFileSync,
7676
}))
7777

78-
// Mocks for tmp module to control temporary file creation and cleanup
79-
const mockRemoveCallback = vi.fn()
78+
// Mocks for tmp module to control temporary file creation
8079
const mockFileSync = vi.fn().mockReturnValue({
8180
name: '/secure/temp/dir/modelResponse-abc123.txt',
82-
removeCallback: mockRemoveCallback,
8381
})
84-
const mockSetGracefulCleanup = vi.fn()
8582

8683
vi.mock('tmp', () => ({
8784
fileSync: mockFileSync,
88-
setGracefulCleanup: mockSetGracefulCleanup,
8985
}))
9086

9187
// Mock MCP and inference modules
@@ -283,42 +279,24 @@ describe('main.ts', () => {
283279
expect(mockProcessExit).toHaveBeenCalledWith(1)
284280
})
285281

286-
it('creates secure temporary files with proper cleanup', async () => {
282+
it('creates temporary files that persist for downstream steps', async () => {
287283
mockInputs({
288284
prompt: 'Test prompt',
289285
'system-prompt': 'You are a test assistant.',
290286
})
291287

292288
await run()
293289

294-
expect(mockSetGracefulCleanup).toHaveBeenCalledOnce()
295-
290+
// Verify temp file is created with keep: true so it persists
296291
expect(mockFileSync).toHaveBeenCalledWith({
297292
prefix: 'modelResponse-',
298293
postfix: '.txt',
294+
keep: true,
299295
})
300296

301297
expect(core.setOutput).toHaveBeenNthCalledWith(2, 'response-file', '/secure/temp/dir/modelResponse-abc123.txt')
302298
expect(mockWriteFileSync).toHaveBeenCalledWith('/secure/temp/dir/modelResponse-abc123.txt', 'Hello, user!', 'utf-8')
303-
expect(mockRemoveCallback).toHaveBeenCalledOnce()
304-
305-
expect(mockProcessExit).toHaveBeenCalledWith(0)
306-
})
307-
308-
it('handles cleanup errors gracefully', async () => {
309-
mockRemoveCallback.mockImplementationOnce(() => {
310-
throw new Error('Cleanup failed')
311-
})
312-
313-
mockInputs({
314-
prompt: 'Test prompt',
315-
'system-prompt': 'You are a test assistant.',
316-
})
317-
318-
await run()
319299

320-
expect(mockRemoveCallback).toHaveBeenCalledOnce()
321-
expect(core.warning).toHaveBeenCalledWith('Failed to cleanup temporary file: Error: Cleanup failed')
322300
expect(mockProcessExit).toHaveBeenCalledWith(0)
323301
})
324302
})

dist/index.js

Lines changed: 5 additions & 17 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

dist/index.js.map

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

eslint.config.mjs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ const compat = new FlatCompat({
1919

2020
export default [
2121
{
22-
ignores: ['**/coverage', '**/dist', '**/linter', '**/node_modules'],
22+
ignores: ['**/coverage', '**/dist', '**/linter', '**/node_modules', 'script/**'],
2323
},
2424
...compat.extends(
2525
'eslint:recommended',

script/mock-inference-server.mjs

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
#!/usr/bin/env node
2+
/**
3+
* A simple mock OpenAI-compatible inference server for CI testing.
4+
* This returns predictable responses without needing real API credentials.
5+
*/
6+
7+
import http from 'http'
8+
9+
const PORT = process.env.MOCK_SERVER_PORT || 3456
10+
11+
const server = http.createServer((req, res) => {
12+
let body = ''
13+
14+
req.on('data', chunk => {
15+
body += chunk.toString()
16+
})
17+
18+
req.on('end', () => {
19+
console.log(`[Mock Server] ${req.method} ${req.url}`)
20+
21+
// Handle chat completions endpoint
22+
if (req.url === '/chat/completions' && req.method === 'POST') {
23+
const request = JSON.parse(body)
24+
const userMessage = request.messages?.find(m => m.role === 'user')?.content || 'No prompt'
25+
26+
const response = {
27+
id: 'mock-completion-id',
28+
object: 'chat.completion',
29+
created: Date.now(),
30+
model: request.model || 'mock-model',
31+
choices: [
32+
{
33+
index: 0,
34+
message: {
35+
role: 'assistant',
36+
content: `Mock response to: "${userMessage.slice(0, 50)}..."`,
37+
},
38+
finish_reason: 'stop',
39+
},
40+
],
41+
usage: {
42+
prompt_tokens: 10,
43+
completion_tokens: 20,
44+
total_tokens: 30,
45+
},
46+
}
47+
48+
res.writeHead(200, {'Content-Type': 'application/json'})
49+
res.end(JSON.stringify(response))
50+
return
51+
}
52+
53+
// Health check endpoint
54+
if (req.url === '/health' || req.url === '/') {
55+
res.writeHead(200, {'Content-Type': 'application/json'})
56+
res.end(JSON.stringify({status: 'ok'}))
57+
return
58+
}
59+
60+
// 404 for unknown routes
61+
res.writeHead(404, {'Content-Type': 'application/json'})
62+
res.end(JSON.stringify({error: 'Not found'}))
63+
})
64+
})
65+
66+
server.listen(PORT, () => {
67+
console.log(`[Mock Server] Listening on http://localhost:${PORT}`)
68+
console.log('[Mock Server] Endpoints:')
69+
console.log(' POST /chat/completions - Mock chat completion')
70+
console.log(' GET /health - Health check')
71+
})

src/main.ts

Lines changed: 5 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,6 @@ import {
1818
* @returns Resolves when the action is complete.
1919
*/
2020
export async function run(): Promise<void> {
21-
let responseFile: tmp.FileResult | null = null
22-
23-
// Set up graceful cleanup for temporary files on process exit
24-
tmp.setGracefulCleanup()
25-
2621
try {
2722
const promptFilePath = core.getInput('prompt-file')
2823
const inputVariables = core.getInput('input')
@@ -101,10 +96,13 @@ export async function run(): Promise<void> {
10196

10297
core.setOutput('response', modelResponse || '')
10398

104-
// Create a secure temporary file instead of using the temp directory directly
105-
responseFile = tmp.fileSync({
99+
// Create a temporary file for the response that persists for downstream steps.
100+
// We use keep: true to prevent automatic cleanup - the file will be cleaned up
101+
// by the runner when the job completes.
102+
const responseFile = tmp.fileSync({
106103
prefix: 'modelResponse-',
107104
postfix: '.txt',
105+
keep: true,
108106
})
109107

110108
core.setOutput('response-file', responseFile.name)
@@ -120,16 +118,6 @@ export async function run(): Promise<void> {
120118
}
121119
// Force exit to prevent hanging on open connections
122120
process.exit(1)
123-
} finally {
124-
// Explicit cleanup of temporary file if it was created
125-
if (responseFile) {
126-
try {
127-
responseFile.removeCallback()
128-
} catch (cleanupError) {
129-
// Log cleanup errors but don't fail the action
130-
core.warning(`Failed to cleanup temporary file: ${cleanupError}`)
131-
}
132-
}
133121
}
134122

135123
// Force exit to prevent hanging on open connections

0 commit comments

Comments
 (0)