@shipfox/api-runners 12.0.0 → 12.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (116) hide show
  1. package/.turbo/turbo-build.log +4 -4
  2. package/CHANGELOG.md +30 -0
  3. package/dist/config.d.ts +3 -0
  4. package/dist/config.d.ts.map +1 -1
  5. package/dist/config.js +24 -2
  6. package/dist/config.js.map +1 -1
  7. package/dist/core/errors.d.ts +4 -0
  8. package/dist/core/errors.d.ts.map +1 -1
  9. package/dist/core/errors.js +6 -0
  10. package/dist/core/errors.js.map +1 -1
  11. package/dist/core/index.d.ts +1 -1
  12. package/dist/core/index.d.ts.map +1 -1
  13. package/dist/core/index.js +1 -1
  14. package/dist/core/index.js.map +1 -1
  15. package/dist/core/rate-limit.d.ts.map +1 -1
  16. package/dist/core/runner-control-sessions.d.ts.map +1 -1
  17. package/dist/core/runner-control-sessions.js +23 -9
  18. package/dist/core/runner-control-sessions.js.map +1 -1
  19. package/dist/core/runner-instances.d.ts +1 -0
  20. package/dist/core/runner-instances.d.ts.map +1 -1
  21. package/dist/core/runner-instances.js.map +1 -1
  22. package/dist/core/runner-labels.d.ts +11 -0
  23. package/dist/core/runner-labels.d.ts.map +1 -0
  24. package/dist/core/runner-labels.js +41 -0
  25. package/dist/core/runner-labels.js.map +1 -0
  26. package/dist/core/runner-sessions.d.ts.map +1 -1
  27. package/dist/core/runner-sessions.js +5 -1
  28. package/dist/core/runner-sessions.js.map +1 -1
  29. package/dist/db/db.d.ts +64 -64
  30. package/dist/db/ephemeral-registration-tokens.d.ts.map +1 -1
  31. package/dist/db/ephemeral-registration-tokens.js +12 -2
  32. package/dist/db/ephemeral-registration-tokens.js.map +1 -1
  33. package/dist/db/reservations.d.ts.map +1 -1
  34. package/dist/db/reservations.js +15 -5
  35. package/dist/db/reservations.js.map +1 -1
  36. package/dist/db/runner-instances.d.ts +1 -0
  37. package/dist/db/runner-instances.d.ts.map +1 -1
  38. package/dist/db/runner-instances.js +6 -4
  39. package/dist/db/runner-instances.js.map +1 -1
  40. package/dist/db/runner-sessions.d.ts.map +1 -1
  41. package/dist/db/runner-sessions.js +12 -1
  42. package/dist/db/runner-sessions.js.map +1 -1
  43. package/dist/db/schema/admin-command-results.d.ts +1 -1
  44. package/dist/db/schema/ephemeral-registration-tokens.d.ts +1 -1
  45. package/dist/db/schema/manual-registration-tokens.d.ts +1 -1
  46. package/dist/db/schema/outbox.d.ts +1 -1
  47. package/dist/db/schema/pending-job-executions.d.ts +3 -3
  48. package/dist/db/schema/provisioner-capability-snapshots.d.ts +3 -3
  49. package/dist/db/schema/provisioner-tokens.d.ts +2 -2
  50. package/dist/db/schema/rate-limits.d.ts +1 -1
  51. package/dist/db/schema/reservations.d.ts +3 -3
  52. package/dist/db/schema/runner-activation-tokens.d.ts +1 -1
  53. package/dist/db/schema/runner-control-sessions.d.ts +2 -2
  54. package/dist/db/schema/runner-instances.d.ts +4 -4
  55. package/dist/db/schema/runner-instances.d.ts.map +1 -1
  56. package/dist/db/schema/runner-instances.js +1 -0
  57. package/dist/db/schema/runner-instances.js.map +1 -1
  58. package/dist/db/schema/runner-sessions.d.ts +4 -4
  59. package/dist/db/schema/running-job-executions.d.ts +5 -5
  60. package/dist/metrics/index.d.ts +2 -1
  61. package/dist/metrics/index.d.ts.map +1 -1
  62. package/dist/metrics/index.js +1 -1
  63. package/dist/metrics/index.js.map +1 -1
  64. package/dist/metrics/instance.d.ts +13 -8
  65. package/dist/metrics/instance.d.ts.map +1 -1
  66. package/dist/metrics/instance.js +8 -0
  67. package/dist/metrics/instance.js.map +1 -1
  68. package/dist/presentation/dto/admin-provisioner-token.d.ts +1 -1
  69. package/dist/presentation/dto/admin-provisioner-token.d.ts.map +1 -1
  70. package/dist/presentation/dto/admin-runner-instances.d.ts +2 -2
  71. package/dist/presentation/dto/admin-runner-instances.d.ts.map +1 -1
  72. package/dist/presentation/routes/poll-demand.d.ts.map +1 -1
  73. package/dist/presentation/routes/poll-demand.js +12 -6
  74. package/dist/presentation/routes/poll-demand.js.map +1 -1
  75. package/dist/presentation/routes/rate-limit.d.ts.map +1 -1
  76. package/dist/presentation/routes/register.d.ts.map +1 -1
  77. package/dist/presentation/routes/register.js +9 -1
  78. package/dist/presentation/routes/register.js.map +1 -1
  79. package/dist/presentation/routes/report-runner-instances.d.ts.map +1 -1
  80. package/dist/presentation/routes/report-runner-instances.js +1 -0
  81. package/dist/presentation/routes/report-runner-instances.js.map +1 -1
  82. package/dist/tsconfig.test.tsbuildinfo +1 -1
  83. package/drizzle/0004_huge_skreet.sql +1 -0
  84. package/drizzle/meta/0004_snapshot.json +2194 -0
  85. package/drizzle/meta/_journal.json +7 -0
  86. package/package.json +9 -9
  87. package/src/config.test.ts +81 -0
  88. package/src/config.ts +39 -2
  89. package/src/core/errors.ts +9 -0
  90. package/src/core/index.ts +1 -0
  91. package/src/core/runner-control-sessions.test.ts +239 -0
  92. package/src/core/runner-control-sessions.ts +29 -10
  93. package/src/core/runner-instances.ts +1 -0
  94. package/src/core/runner-labels.test.ts +53 -0
  95. package/src/core/runner-labels.ts +51 -0
  96. package/src/core/runner-sessions.test.ts +34 -1
  97. package/src/core/runner-sessions.ts +8 -1
  98. package/src/db/ephemeral-registration-tokens.ts +15 -1
  99. package/src/db/reservations.test.ts +169 -0
  100. package/src/db/reservations.ts +42 -8
  101. package/src/db/runner-instances.test.ts +45 -1
  102. package/src/db/runner-instances.ts +7 -2
  103. package/src/db/runner-sessions.ts +14 -1
  104. package/src/db/schema/runner-instances.ts +3 -0
  105. package/src/metrics/index.ts +2 -0
  106. package/src/metrics/instance.ts +18 -0
  107. package/src/presentation/routes/poll-demand.test.ts +99 -3
  108. package/src/presentation/routes/poll-demand.ts +18 -6
  109. package/src/presentation/routes/register.test.ts +116 -2
  110. package/src/presentation/routes/register.ts +7 -0
  111. package/src/presentation/routes/report-runner-instances.test.ts +33 -0
  112. package/src/presentation/routes/report-runner-instances.ts +1 -0
  113. package/src/presentation/routes/runner-enrollment.test.ts +65 -5
  114. package/src/temporal/activities/maintenance-activities.test.ts +24 -31
  115. package/test/env.ts +1 -0
  116. package/tsconfig.build.tsbuildinfo +1 -1
@@ -1,11 +1,12 @@
1
1
  import {AUTH_PROVISIONER_TOKEN, AUTH_USER} from '@shipfox/api-auth-context';
2
2
  import type {RunnerToolCapabilitiesDto} from '@shipfox/api-runners-dto';
3
3
  import type {AuthMethod} from '@shipfox/node-fastify';
4
- import {closeApp, createApp} from '@shipfox/node-fastify';
4
+ import {ClientError, closeApp, createApp} from '@shipfox/node-fastify';
5
5
  import {generateOpaqueToken} from '@shipfox/node-tokens';
6
6
  import {eq, sql} from 'drizzle-orm';
7
- import type {FastifyInstance} from 'fastify';
7
+ import type {FastifyInstance, FastifyReply, FastifyRequest} from 'fastify';
8
8
  import {config} from '#config.js';
9
+ import {RunnerLabelsReservedError} from '#core/errors.js';
9
10
  import {hashRunnersRateLimitIdentifier} from '#core/rate-limit.js';
10
11
  import {db} from '#db/db.js';
11
12
  import {revokeManualRegistrationToken} from '#db/manual-registration-tokens.js';
@@ -19,9 +20,11 @@ import {
19
20
  fakeRunnerSessionAuthMethod,
20
21
  getRunnerSessionTokenClaims,
21
22
  manualRegistrationTokenFactory,
23
+ provisionerTokenFactory,
22
24
  runnersTestAuthClient,
23
25
  } from '#test/index.js';
24
26
  import {createRunnerRoutes} from './index.js';
27
+ import {createRegisterRoute} from './register.js';
25
28
 
26
29
  const fakeUserAuth: AuthMethod = {
27
30
  name: AUTH_USER,
@@ -123,6 +126,38 @@ describe('POST /runners/register', () => {
123
126
  expect(session?.toolCapabilitiesReportedAt).toBeInstanceOf(Date);
124
127
  });
125
128
 
129
+ it('strips reserved labels from manual registration', async () => {
130
+ const res = await app.inject({
131
+ method: 'POST',
132
+ url: '/runners/register',
133
+ headers: {authorization: `Bearer ${rawToken}`},
134
+ payload: {labels: ['linux', 'shipfox-managed', 'x64']},
135
+ });
136
+
137
+ const [session] = await db()
138
+ .select()
139
+ .from(runnerSessions)
140
+ .where(eq(runnerSessions.id, res.json().session_id));
141
+
142
+ expect(res.statusCode).toBe(200);
143
+ expect(session?.labels).toEqual(['linux', 'x64']);
144
+ });
145
+
146
+ it('returns a distinct error when all manual registration labels are reserved', async () => {
147
+ const res = await app.inject({
148
+ method: 'POST',
149
+ url: '/runners/register',
150
+ headers: {authorization: `Bearer ${rawToken}`},
151
+ payload: {labels: ['shipfox-managed']},
152
+ });
153
+
154
+ expect(res.statusCode).toBe(400);
155
+ expect(res.json()).toMatchObject({
156
+ code: 'runner-labels-reserved',
157
+ details: {labels: ['shipfox-managed']},
158
+ });
159
+ });
160
+
126
161
  it('exchanges an ephemeral registration token for a one-claim runner session', async () => {
127
162
  const ephemeralRawToken = generateOpaqueToken('ephemeralRegistrationToken');
128
163
  const token = await ephemeralRegistrationTokenFactory.create(
@@ -165,6 +200,65 @@ describe('POST /runners/register', () => {
165
200
  expect(consumed?.consumedSessionId).toBe(body.session_id);
166
201
  });
167
202
 
203
+ it('strips reserved labels from ephemeral registration', async () => {
204
+ const ephemeralRawToken = generateOpaqueToken('ephemeralRegistrationToken');
205
+ const provisioner = await provisionerTokenFactory.create({
206
+ scope: 'workspace',
207
+ workspaceId,
208
+ });
209
+ const token = await ephemeralRegistrationTokenFactory.create(
210
+ {workspaceId, provisionerId: provisioner.id},
211
+ {transient: {rawToken: ephemeralRawToken}},
212
+ );
213
+
214
+ const res = await app.inject({
215
+ method: 'POST',
216
+ url: '/runners/register',
217
+ headers: {authorization: `Bearer ${ephemeralRawToken}`},
218
+ payload: {labels: ['linux', 'shipfox-managed']},
219
+ });
220
+
221
+ const [session] = await db()
222
+ .select()
223
+ .from(runnerSessions)
224
+ .where(eq(runnerSessions.id, res.json().session_id));
225
+
226
+ expect(res.statusCode).toBe(200);
227
+ expect(session).toMatchObject({
228
+ id: res.json().session_id,
229
+ provisionerId: token.provisionerId,
230
+ labels: ['linux'],
231
+ });
232
+ });
233
+
234
+ it('preserves reserved labels for installation-scope ephemeral registration', async () => {
235
+ const ephemeralRawToken = generateOpaqueToken('ephemeralRegistrationToken');
236
+ const provisioner = await provisionerTokenFactory.create({scope: 'installation'});
237
+ const token = await ephemeralRegistrationTokenFactory.create(
238
+ {workspaceId, provisionerId: provisioner.id},
239
+ {transient: {rawToken: ephemeralRawToken}},
240
+ );
241
+
242
+ const res = await app.inject({
243
+ method: 'POST',
244
+ url: '/runners/register',
245
+ headers: {authorization: `Bearer ${ephemeralRawToken}`},
246
+ payload: {labels: ['linux', 'shipfox-managed']},
247
+ });
248
+
249
+ const [session] = await db()
250
+ .select()
251
+ .from(runnerSessions)
252
+ .where(eq(runnerSessions.id, res.json().session_id));
253
+
254
+ expect(res.statusCode).toBe(200);
255
+ expect(session).toMatchObject({
256
+ id: res.json().session_id,
257
+ provisionerId: token.provisionerId,
258
+ labels: ['linux', 'shipfox-managed'],
259
+ });
260
+ });
261
+
168
262
  it('rejects malformed capability reports without creating a runner session', async () => {
169
263
  const res = await app.inject({
170
264
  method: 'POST',
@@ -418,6 +512,26 @@ describe('POST /runners/register', () => {
418
512
  expect(res.statusCode).toBe(200);
419
513
  });
420
514
 
515
+ it('maps reserved-label-only registration failures to a public error code', () => {
516
+ const route = createRegisterRoute(runnersTestAuthClient);
517
+
518
+ try {
519
+ route.errorHandler?.(
520
+ new RunnerLabelsReservedError(['shipfox-managed']),
521
+ {} as FastifyRequest,
522
+ {} as FastifyReply,
523
+ );
524
+ throw new Error('Expected register route error handler to throw');
525
+ } catch (error) {
526
+ expect(error).toBeInstanceOf(ClientError);
527
+ expect(error).toMatchObject({
528
+ code: 'runner-labels-reserved',
529
+ details: {labels: ['shipfox-managed']},
530
+ status: 400,
531
+ });
532
+ }
533
+ });
534
+
421
535
  async function seedEphemeralRegisterRateLimit(
422
536
  tokenId: string,
423
537
  seedCount: number,
@@ -5,6 +5,7 @@ import {
5
5
  EmptyRunnerLabelsError,
6
6
  RegistrationTokenConsumedError,
7
7
  RegistrationTokenExpiredError,
8
+ RunnerLabelsReservedError,
8
9
  } from '#core/errors.js';
9
10
  import {registerRunnerSession} from '#core/runner-sessions.js';
10
11
  import {getRunnerContext} from '#presentation/auth/index.js';
@@ -23,6 +24,12 @@ export function createRegisterRoute(auth: AuthInterModuleClient) {
23
24
  },
24
25
  preHandler: createEphemeralRegisterRateLimitPreHandler(),
25
26
  errorHandler: (error, request) => {
27
+ if (error instanceof RunnerLabelsReservedError) {
28
+ throw new ClientError(error.message, 'runner-labels-reserved', {
29
+ details: {labels: error.labels},
30
+ status: 400,
31
+ });
32
+ }
26
33
  if (error instanceof EmptyRunnerLabelsError) {
27
34
  throw new ClientError(error.message, 'empty-runner-labels', {status: 400});
28
35
  }
@@ -113,6 +113,39 @@ describe('POST /provisioners/runner-instances/report', () => {
113
113
  expect(rows[0]?.reportedAt.toISOString()).toBe(reportedAt);
114
114
  });
115
115
 
116
+ it('strips reserved labels from workspace runner reports', async () => {
117
+ const res = await app.inject({
118
+ method: 'POST',
119
+ url: '/provisioners/runner-instances/report',
120
+ headers: {authorization: `Bearer ${VALID_PROVISIONER_TOKEN}`},
121
+ payload: {
122
+ events: [
123
+ {
124
+ provider_runner_id: 'provisioned-runner-reserved-label',
125
+ labels: ['linux', 'shipfox-managed'],
126
+ state: 'starting',
127
+ reported_at: new Date('2025-01-01T00:00:00.000Z').toISOString(),
128
+ provider_kind: 'docker',
129
+ },
130
+ ],
131
+ },
132
+ });
133
+
134
+ const [row] = await db()
135
+ .select()
136
+ .from(providerRunners)
137
+ .where(
138
+ and(
139
+ eq(providerRunners.workspaceId, workspaceId),
140
+ eq(providerRunners.provisionerId, provisionerTokenId),
141
+ eq(providerRunners.providerRunnerId, 'provisioned-runner-reserved-label'),
142
+ ),
143
+ );
144
+
145
+ expect(res.statusCode).toBe(200);
146
+ expect(row?.labels).toEqual(['linux']);
147
+ });
148
+
116
149
  it('returns 400 when the batch exceeds the DTO limit', async () => {
117
150
  const event = {
118
151
  provider_runner_id: 'provisioned-runner-1',
@@ -20,6 +20,7 @@ export const reportRunnerInstancesRoute = defineRoute({
20
20
  handler: async (request) => {
21
21
  const context = requireProvisionerContext(request);
22
22
  const result = await reportRunnerInstances({
23
+ scope: context.scope,
23
24
  workspaceId: context.scope === 'workspace' ? context.workspaceId : null,
24
25
  provisionerId: context.provisionerTokenId,
25
26
  events: request.body.events.map((event) => ({
@@ -20,6 +20,7 @@ import {
20
20
  import {
21
21
  fakeLeaseTokenAuthMethod,
22
22
  fakeRunnerSessionAuthMethod,
23
+ provisionerTokenFactory,
23
24
  runnersTestAuthClient,
24
25
  } from '#test/index.js';
25
26
  import {createRunnerRoutes} from './index.js';
@@ -29,15 +30,27 @@ import {
29
30
  } from './runner-enrollment.js';
30
31
 
31
32
  const token = 'provisioner-test-token';
33
+ const workspaceToken = 'workspace-provisioner-test-token';
32
34
  const fakeUserAuth: AuthMethod = {name: AUTH_USER, authenticate: () => Promise.resolve()};
33
35
 
34
36
  describe('runner enrollment control plane', () => {
35
37
  let app: FastifyInstance;
36
38
  let provisionerId: string;
39
+ let workspaceProvisionerId: string;
40
+ let workspaceProvisionerWorkspaceId: string;
37
41
  const provisionerAuth: AuthMethod = {
38
42
  name: AUTH_PROVISIONER_TOKEN,
39
43
  authenticate: (request: FastifyRequest) => {
40
- if (extractBearerToken(request.headers.authorization) !== token)
44
+ const rawToken = extractBearerToken(request.headers.authorization);
45
+ if (rawToken === workspaceToken) {
46
+ setProvisionerContext(request, {
47
+ scope: 'workspace',
48
+ workspaceId: workspaceProvisionerWorkspaceId,
49
+ provisionerTokenId: workspaceProvisionerId,
50
+ });
51
+ return Promise.resolve();
52
+ }
53
+ if (rawToken !== token)
41
54
  throw new ClientError('Invalid provisioner token', 'unauthorized', {status: 401});
42
55
  setProvisionerContext(request, {scope: 'installation', provisionerTokenId: provisionerId});
43
56
  return Promise.resolve();
@@ -64,8 +77,17 @@ describe('runner enrollment control plane', () => {
64
77
  await closeApp();
65
78
  });
66
79
 
67
- beforeEach(() => {
68
- provisionerId = crypto.randomUUID();
80
+ beforeEach(async () => {
81
+ const provisioner = await provisionerTokenFactory.create({scope: 'installation'});
82
+ const workspaceProvisioner = await provisionerTokenFactory.create({
83
+ scope: 'workspace',
84
+ });
85
+ if (workspaceProvisioner.scope !== 'workspace') {
86
+ throw new Error('Expected a workspace provisioner token');
87
+ }
88
+ provisionerId = provisioner.id;
89
+ workspaceProvisionerId = workspaceProvisioner.id;
90
+ workspaceProvisionerWorkspaceId = workspaceProvisioner.workspaceId;
69
91
  });
70
92
 
71
93
  it('uses the requested assignment wait below the server cap and caps larger requests', () => {
@@ -112,7 +134,11 @@ describe('runner enrollment control plane', () => {
112
134
  method: 'POST',
113
135
  url: '/runner-control/enrollment',
114
136
  headers: {authorization: `Bearer ${controlToken}`},
115
- payload: {labels: ['Linux', 'linux'], provider_kind: 'docker', protocol_version: '1'},
137
+ payload: {
138
+ labels: ['Linux', 'linux', 'shipfox-managed'],
139
+ provider_kind: 'docker',
140
+ protocol_version: '1',
141
+ },
116
142
  });
117
143
  expect(enrolled.statusCode).toBe(200);
118
144
  expect(enrolled.json()).toEqual({activation_token: null});
@@ -131,7 +157,7 @@ describe('runner enrollment control plane', () => {
131
157
  .where(eq(providerRunners.id, runner.runner_instance_id));
132
158
  expect(instance).toMatchObject({
133
159
  provisionerId,
134
- labels: ['linux'],
160
+ labels: ['linux', 'shipfox-managed'],
135
161
  providerRunnerId: 'container-1',
136
162
  state: 'running',
137
163
  protocolVersion: '1',
@@ -150,6 +176,40 @@ describe('runner enrollment control plane', () => {
150
176
  expect(jobs.statusCode).toBe(401);
151
177
  });
152
178
 
179
+ it('strips reserved labels when a workspace provisioner enrolls a runner', async () => {
180
+ const created = await app.inject({
181
+ method: 'POST',
182
+ url: '/provisioners/runner-instances/batch',
183
+ headers: {authorization: `Bearer ${workspaceToken}`},
184
+ payload: {runner_instances: [{}]},
185
+ });
186
+ const runner = created.json().runner_instances[0];
187
+ const exchanged = await app.inject({
188
+ method: 'POST',
189
+ url: '/runner-enrollment/exchange',
190
+ payload: {bootstrap_token: runner.bootstrap_token},
191
+ });
192
+
193
+ const enrolled = await app.inject({
194
+ method: 'POST',
195
+ url: '/runner-control/enrollment',
196
+ headers: {authorization: `Bearer ${exchanged.json().control_session_token}`},
197
+ payload: {
198
+ labels: ['linux', 'shipfox-managed'],
199
+ provider_kind: 'docker',
200
+ protocol_version: '1',
201
+ },
202
+ });
203
+
204
+ const [instance] = await db()
205
+ .select()
206
+ .from(providerRunners)
207
+ .where(eq(providerRunners.id, runner.runner_instance_id));
208
+
209
+ expect(enrolled.statusCode).toBe(200);
210
+ expect(instance?.labels).toEqual(['linux']);
211
+ });
212
+
153
213
  it('promotes an intended reservation during enrollment and returns its activation token', async () => {
154
214
  const workspaceId = crypto.randomUUID();
155
215
  const [reservation] = await db()
@@ -1,18 +1,3 @@
1
- import {
2
- deleteExpiredEphemeralRegistrationTokens,
3
- deleteExpiredRunnerReservations,
4
- deleteExpiredRunnerSessions,
5
- detectAndExpireStuckJobs,
6
- reapStaleRunnerInstances,
7
- } from '#core/maintenance.js';
8
- import {
9
- deleteExpiredEphemeralRegistrationTokensActivity,
10
- deleteExpiredReservationsActivity,
11
- deleteExpiredRunnerSessionsActivity,
12
- detectAndExpireStuckJobsActivity,
13
- reapStaleRunnerInstancesActivity,
14
- } from './maintenance-activities.js';
15
-
16
1
  vi.mock('#core/maintenance.js', () => ({
17
2
  deleteExpiredEphemeralRegistrationTokens: vi.fn(),
18
3
  deleteExpiredRunnerReservations: vi.fn(),
@@ -21,46 +6,52 @@ vi.mock('#core/maintenance.js', () => ({
21
6
  reapStaleRunnerInstances: vi.fn(),
22
7
  }));
23
8
 
24
- beforeEach(() => {
9
+ let maintenance: typeof import('#core/maintenance.js');
10
+ let activities: typeof import('./maintenance-activities.js');
11
+
12
+ beforeEach(async () => {
13
+ vi.resetModules();
14
+ maintenance = await import('#core/maintenance.js');
15
+ activities = await import('./maintenance-activities.js');
25
16
  vi.clearAllMocks();
26
17
  });
27
18
 
28
19
  describe('detectAndExpireStuckJobsActivity', () => {
29
20
  it('delegates to core maintenance', async () => {
30
- vi.mocked(detectAndExpireStuckJobs).mockResolvedValueOnce({expired: 2});
21
+ vi.mocked(maintenance.detectAndExpireStuckJobs).mockResolvedValueOnce({expired: 2});
31
22
 
32
- const result = await detectAndExpireStuckJobsActivity({thresholdSeconds: 180});
23
+ const result = await activities.detectAndExpireStuckJobsActivity({thresholdSeconds: 180});
33
24
 
34
25
  expect(result).toEqual({expired: 2});
35
- expect(detectAndExpireStuckJobs).toHaveBeenCalledWith({thresholdSeconds: 180});
26
+ expect(maintenance.detectAndExpireStuckJobs).toHaveBeenCalledWith({thresholdSeconds: 180});
36
27
  });
37
28
  });
38
29
 
39
30
  describe('deleteExpiredReservationsActivity', () => {
40
31
  it('delegates to core maintenance', async () => {
41
- vi.mocked(deleteExpiredRunnerReservations).mockResolvedValueOnce({deleted: 3});
32
+ vi.mocked(maintenance.deleteExpiredRunnerReservations).mockResolvedValueOnce({deleted: 3});
42
33
 
43
- const result = await deleteExpiredReservationsActivity({limit: 50});
34
+ const result = await activities.deleteExpiredReservationsActivity({limit: 50});
44
35
 
45
36
  expect(result).toEqual({deleted: 3});
46
- expect(deleteExpiredRunnerReservations).toHaveBeenCalledWith({limit: 50});
37
+ expect(maintenance.deleteExpiredRunnerReservations).toHaveBeenCalledWith({limit: 50});
47
38
  });
48
39
  });
49
40
 
50
41
  describe('reapStaleRunnerInstancesActivity', () => {
51
42
  it('delegates to core maintenance', async () => {
52
- vi.mocked(reapStaleRunnerInstances).mockResolvedValueOnce({
43
+ vi.mocked(maintenance.reapStaleRunnerInstances).mockResolvedValueOnce({
53
44
  reaped: 4,
54
45
  reservationsReleased: 2,
55
46
  });
56
47
 
57
- const result = await reapStaleRunnerInstancesActivity({
48
+ const result = await activities.reapStaleRunnerInstancesActivity({
58
49
  thresholdSeconds: 300,
59
50
  limit: 100,
60
51
  });
61
52
 
62
53
  expect(result).toEqual({reaped: 4, reservationsReleased: 2});
63
- expect(reapStaleRunnerInstances).toHaveBeenCalledWith({
54
+ expect(maintenance.reapStaleRunnerInstances).toHaveBeenCalledWith({
64
55
  thresholdSeconds: 300,
65
56
  limit: 100,
66
57
  });
@@ -69,16 +60,16 @@ describe('reapStaleRunnerInstancesActivity', () => {
69
60
 
70
61
  describe('deleteExpiredRunnerSessionsActivity', () => {
71
62
  it('delegates to core maintenance', async () => {
72
- vi.mocked(deleteExpiredRunnerSessions).mockResolvedValueOnce({deleted: 4});
63
+ vi.mocked(maintenance.deleteExpiredRunnerSessions).mockResolvedValueOnce({deleted: 4});
73
64
 
74
- const result = await deleteExpiredRunnerSessionsActivity({
65
+ const result = await activities.deleteExpiredRunnerSessionsActivity({
75
66
  manualRetentionDays: 30,
76
67
  ephemeralRetentionDays: 7,
77
68
  limit: 25,
78
69
  });
79
70
 
80
71
  expect(result).toEqual({deleted: 4});
81
- expect(deleteExpiredRunnerSessions).toHaveBeenCalledWith({
72
+ expect(maintenance.deleteExpiredRunnerSessions).toHaveBeenCalledWith({
82
73
  manualRetentionDays: 30,
83
74
  ephemeralRetentionDays: 7,
84
75
  limit: 25,
@@ -88,15 +79,17 @@ describe('deleteExpiredRunnerSessionsActivity', () => {
88
79
 
89
80
  describe('deleteExpiredEphemeralRegistrationTokensActivity', () => {
90
81
  it('delegates to core maintenance', async () => {
91
- vi.mocked(deleteExpiredEphemeralRegistrationTokens).mockResolvedValueOnce({deleted: 6});
82
+ vi.mocked(maintenance.deleteExpiredEphemeralRegistrationTokens).mockResolvedValueOnce({
83
+ deleted: 6,
84
+ });
92
85
 
93
- const result = await deleteExpiredEphemeralRegistrationTokensActivity({
86
+ const result = await activities.deleteExpiredEphemeralRegistrationTokensActivity({
94
87
  retentionDays: 7,
95
88
  limit: 25,
96
89
  });
97
90
 
98
91
  expect(result).toEqual({deleted: 6});
99
- expect(deleteExpiredEphemeralRegistrationTokens).toHaveBeenCalledWith({
92
+ expect(maintenance.deleteExpiredEphemeralRegistrationTokens).toHaveBeenCalledWith({
100
93
  retentionDays: 7,
101
94
  limit: 25,
102
95
  });
package/test/env.ts CHANGED
@@ -4,6 +4,7 @@ process.env.POSTGRES_USERNAME ??= 'shipfox';
4
4
  process.env.POSTGRES_PASSWORD ??= 'password';
5
5
  process.env.POSTGRES_DATABASE = 'api_test';
6
6
  process.env.POSTGRES_MAX_CONNECTIONS ??= '5';
7
+ process.env.RUNNER_RESERVED_LABELS ??= 'shipfox-managed';
7
8
  process.env.WORKSPACE_JWT_SECRET = 'test-secret';
8
9
  process.env.AUTH_ROOT_KEY = 'MDEyMzQ1Njc4OWFiY2RlZjAxMjM0NTY3ODlhYmNkZWY=';
9
10
  process.env.TZ = 'UTC';