@pikku/core 0.12.97 → 0.12.98

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.
@@ -244,6 +244,65 @@ describe('startVirtualUserRun', () => {
244
244
  assert.equal(started[0].disposition, 'accountable')
245
245
  })
246
246
 
247
+ // The case NODE_ENV cannot answer: a staging environment that is a production
248
+ // mirror runs NODE_ENV=production too, and refusing every disposition there
249
+ // refuses them on the one environment they exist to be used on.
250
+ test('a non-production environment allows a probing disposition', async () => {
251
+ const { store, started } = runStore()
252
+ await startVirtualUserRun({
253
+ store,
254
+ personas,
255
+ config: { nodeEnv: 'production' },
256
+ environments: { staging: {}, production: { production: true } },
257
+ environment: 'staging',
258
+ persona: 'susan',
259
+ disposition: 'adversarial',
260
+ })
261
+ assert.equal(started[0].disposition, 'adversarial')
262
+ })
263
+
264
+ test('the configured production environment still refuses one', async () => {
265
+ const { store, started } = runStore()
266
+ await assert.rejects(
267
+ startVirtualUserRun({
268
+ store,
269
+ personas,
270
+ config: { nodeEnv: 'development' },
271
+ environments: { staging: {}, production: { production: true } },
272
+ environment: 'production',
273
+ persona: 'susan',
274
+ disposition: 'adversarial',
275
+ }),
276
+ /Only the 'accountable' disposition may run against production/
277
+ )
278
+ assert.equal(started.length, 0)
279
+ })
280
+
281
+ // An environment nobody can name is one whose data nobody can vouch for.
282
+ // PIKKU_ENV is cleared rather than passed as undefined: it is the default the
283
+ // parameter falls back to, so leaving it set would test the wrong thing.
284
+ test('an unresolved environment is treated as production', async () => {
285
+ const { store, started } = runStore()
286
+ const pikkuEnv = process.env.PIKKU_ENV
287
+ delete process.env.PIKKU_ENV
288
+ try {
289
+ await assert.rejects(
290
+ startVirtualUserRun({
291
+ store,
292
+ personas,
293
+ config: { nodeEnv: 'development' },
294
+ environments: { staging: {}, production: { production: true } },
295
+ persona: 'susan',
296
+ disposition: 'adversarial',
297
+ }),
298
+ /Only the 'accountable' disposition may run against production/
299
+ )
300
+ } finally {
301
+ if (pikkuEnv !== undefined) process.env.PIKKU_ENV = pikkuEnv
302
+ }
303
+ assert.equal(started.length, 0)
304
+ })
305
+
247
306
  test('refuses a persona that is declared as acted upon', async () => {
248
307
  const { store, started } = runStore()
249
308
  await assert.rejects(
@@ -11,6 +11,7 @@ import { prepareVirtualUserRun } from './prepare-virtual-user-run.js'
11
11
  import { runVirtualUser as runVirtualUserEngine } from './run-virtual-user.js'
12
12
  import { personaVirtualUserTarget } from './virtual-user-target.js'
13
13
  import type { SchemaMap } from './virtual-user-derive.js'
14
+ import type { PersonaEnvironment } from '../persona/persona-environments.js'
14
15
  import { PRODUCTION_DISPOSITION } from './virtual-user.types.js'
15
16
  import type {
16
17
  StepRecord,
@@ -160,8 +161,13 @@ export interface StartVirtualUserRunParams {
160
161
  /**
161
162
  * The app's config, read only for `nodeEnv` — structural because an
162
163
  * application's Config is its own interface and need not declare it at all.
164
+ * The fallback signal, used only by a project that configures no environments.
163
165
  */
164
166
  config: { nodeEnv?: string } | undefined
167
+ /** `environments` from pikku.config.json, as generated beside the personas. */
168
+ environments?: Readonly<Record<string, PersonaEnvironment>>
169
+ /** Which of them this process is. Defaults to `PIKKU_ENV`. */
170
+ environment?: string
165
171
  persona: string
166
172
  disposition?: string
167
173
  seed?: number
@@ -181,6 +187,33 @@ export interface StartedVirtualUserRun {
181
187
  memory: Record<string, string>
182
188
  }
183
189
 
190
+ /**
191
+ * Whether this process is running against production, for the disposition rule.
192
+ *
193
+ * The configured environment wins over `NODE_ENV` because they answer different
194
+ * questions. A deployment whose staging is a production *mirror* runs
195
+ * `NODE_ENV=production` there too — keying on it refuses every disposition on
196
+ * the one environment they exist to be used on. `PIKKU_ENV` names which of the
197
+ * configured environments this is, which is the question actually being asked,
198
+ * and it is the same signal `personaEnvironmentRefusal` already checks at
199
+ * sign-in.
200
+ *
201
+ * Unresolved is treated as production: an environment nobody can name is one
202
+ * whose data nobody can vouch for. `NODE_ENV` remains the answer only for a
203
+ * project that configures no environments at all, which has no production
204
+ * environment declared for this to be wrong about.
205
+ */
206
+ const isProductionRun = (
207
+ config: { nodeEnv?: string } | undefined,
208
+ environments: Readonly<Record<string, PersonaEnvironment>> | undefined,
209
+ environment: string | undefined
210
+ ): boolean => {
211
+ if (!environments || Object.keys(environments).length === 0) {
212
+ return config?.nodeEnv === 'production'
213
+ }
214
+ return environment ? Boolean(environments[environment]?.production) : true
215
+ }
216
+
184
217
  /**
185
218
  * Resolves a request against the declaration and records the run.
186
219
  *
@@ -192,6 +225,8 @@ export const startVirtualUserRun = async ({
192
225
  store,
193
226
  personas,
194
227
  config,
228
+ environments,
229
+ environment = process.env.PIKKU_ENV,
195
230
  persona: personaId,
196
231
  disposition: requested,
197
232
  seed: requestedSeed,
@@ -210,8 +245,8 @@ export const startVirtualUserRun = async ({
210
245
  // does wrong, which is not a thing to do to real customers' data. Checked
211
246
  // against the effective disposition, so an override cannot smuggle one in.
212
247
  if (
213
- config?.nodeEnv === 'production' &&
214
- disposition !== PRODUCTION_DISPOSITION
248
+ disposition !== PRODUCTION_DISPOSITION &&
249
+ isProductionRun(config, environments, environment)
215
250
  ) {
216
251
  throw new Error(
217
252
  `Only the '${PRODUCTION_DISPOSITION}' disposition may run against production; "${personaId}" is ${disposition}`