@pikku/core 0.12.121 → 0.12.122

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.
package/CHANGELOG.md CHANGED
@@ -1,3 +1,10 @@
1
+ ## 0.12.122
2
+
3
+ ### Patch Changes
4
+
5
+ - 3511717: A virtual user run refused for an unknown or acted-upon persona now answers 400, and one refused for a probing disposition in production answers 403, carrying the reason instead of a bare 500 errorId.
6
+ - 3511717: Enabling a virtual user schedule whose disposition production refuses is now refused when written (403), rather than saved and then failing on every tick with nobody watching. Disabling one, or editing one that is off, is always allowed.
7
+
1
8
  ## 0.12.121
2
9
 
3
10
  ### Patch Changes
@@ -210,15 +210,26 @@ export interface WriteVirtualUserScheduleParams {
210
210
  minIntervalMs?: number;
211
211
  maxIntervalMs?: number;
212
212
  nextRunAt?: string;
213
+ /** The same three signals `startVirtualUserRun` reads to decide production. */
214
+ config?: {
215
+ nodeEnv?: string;
216
+ };
217
+ environments?: Readonly<Record<string, PersonaEnvironment>>;
218
+ environment?: string;
213
219
  }
214
220
  /**
215
221
  * Writes a persona's cadence.
216
222
  *
217
- * Applies the same rule `startVirtualUserRun` enforces, at the point the row is
223
+ * Applies the same rules `startVirtualUserRun` enforces, at the point the row is
218
224
  * written rather than every hour afterwards: an acted-upon persona has no
219
- * session, so a cadence for one is a tick that can only ever fail to start.
225
+ * session, and an enabled cadence whose disposition production refuses, so
226
+ * either is a tick that can only ever fail to start, with nobody watching.
227
+ *
228
+ * The disposition checked is the one the row will hold, since a field left out
229
+ * keeps what it had: enabling a row saved earlier as adversarial is refused
230
+ * just as saving one would be.
220
231
  */
221
- export declare const writeVirtualUserSchedule: ({ store, personas, persona, enabled, disposition, goals, budget, minIntervalMs, maxIntervalMs, nextRunAt, }: WriteVirtualUserScheduleParams) => Promise<VirtualUserScheduleRecord>;
232
+ export declare const writeVirtualUserSchedule: ({ store, personas, persona, enabled, disposition, goals, budget, minIntervalMs, maxIntervalMs, nextRunAt, config, environments, environment, }: WriteVirtualUserScheduleParams) => Promise<VirtualUserScheduleRecord>;
222
233
  /** What a due schedule asks `runVirtualUser` for. */
223
234
  export declare const virtualUserScheduleRunInput: (schedule: VirtualUserScheduleRecord) => {
224
235
  persona: string;
@@ -1,3 +1,4 @@
1
+ import { BadRequestError, ForbiddenError } from '../../errors/errors.js';
1
2
  import { prepareVirtualUserRun } from './prepare-virtual-user-run.js';
2
3
  import { runVirtualUser as runVirtualUserEngine } from './run-virtual-user.js';
3
4
  import { personaVirtualUserTarget } from './virtual-user-target.js';
@@ -59,10 +60,10 @@ export const signInPathFor = (configured, plugin) => {
59
60
  export const runnablePersona = (personas, personaId) => {
60
61
  const persona = personas[personaId];
61
62
  if (!persona) {
62
- throw new Error(`Unknown persona "${personaId}" — declare it with definePersonas()`);
63
+ throw new BadRequestError(`Unknown persona "${personaId}" — declare it with definePersonas()`);
63
64
  }
64
65
  if (!persona.runnable) {
65
- throw new Error(`Persona "${personaId}" is declared as acted upon, never run`);
66
+ throw new BadRequestError(`Persona "${personaId}" is declared as acted upon, never run`);
66
67
  }
67
68
  return persona;
68
69
  };
@@ -106,6 +107,14 @@ const isProductionRun = (config, environments, environment) => {
106
107
  }
107
108
  return environment ? Boolean(environments[environment]?.production) : true;
108
109
  };
110
+ // Every disposition other than this one exists to find out what the product
111
+ // does wrong, which is not a thing to do to real customers' data.
112
+ const refuseInProduction = (personaId, disposition, config, environments, environment) => {
113
+ if (disposition !== PRODUCTION_DISPOSITION &&
114
+ isProductionRun(config, environments, environment)) {
115
+ throw new ForbiddenError(`Only the '${PRODUCTION_DISPOSITION}' disposition may run against production; "${personaId}" is ${disposition}`);
116
+ }
117
+ };
109
118
  /**
110
119
  * Resolves a request against the declaration and records the run.
111
120
  *
@@ -119,13 +128,9 @@ export const startVirtualUserRun = async ({ store, personas, config, environment
119
128
  const disposition = (requested ??
120
129
  persona.disposition ??
121
130
  'realistic');
122
- // Every disposition other than this one exists to find out what the product
123
- // does wrong, which is not a thing to do to real customers' data. Checked
124
- // against the effective disposition, so an override cannot smuggle one in.
125
- if (disposition !== PRODUCTION_DISPOSITION &&
126
- isProductionRun(config, environments, environment)) {
127
- throw new Error(`Only the '${PRODUCTION_DISPOSITION}' disposition may run against production; "${personaId}" is ${disposition}`);
128
- }
131
+ // Checked against the effective disposition, so an override cannot smuggle
132
+ // one in.
133
+ refuseInProduction(personaId, disposition, config, environments, environment);
129
134
  // Seeded here rather than inside the engine so the record carries the seed
130
135
  // even if the run dies before returning — an unreproducible crash costs the
131
136
  // most.
@@ -237,17 +242,29 @@ export const serializeVirtualUserSchedule = (schedule, personas) => {
237
242
  /**
238
243
  * Writes a persona's cadence.
239
244
  *
240
- * Applies the same rule `startVirtualUserRun` enforces, at the point the row is
245
+ * Applies the same rules `startVirtualUserRun` enforces, at the point the row is
241
246
  * written rather than every hour afterwards: an acted-upon persona has no
242
- * session, so a cadence for one is a tick that can only ever fail to start.
247
+ * session, and an enabled cadence whose disposition production refuses, so
248
+ * either is a tick that can only ever fail to start, with nobody watching.
249
+ *
250
+ * The disposition checked is the one the row will hold, since a field left out
251
+ * keeps what it had: enabling a row saved earlier as adversarial is refused
252
+ * just as saving one would be.
243
253
  */
244
- export const writeVirtualUserSchedule = async ({ store, personas, persona, enabled, disposition, goals, budget, minIntervalMs, maxIntervalMs, nextRunAt, }) => {
254
+ export const writeVirtualUserSchedule = async ({ store, personas, persona, enabled, disposition, goals, budget, minIntervalMs, maxIntervalMs, nextRunAt, config, environments, environment = process.env.PIKKU_ENV, }) => {
245
255
  const scheduleStore = requireVirtualUserScheduleStore(store);
246
- runnablePersona(personas, persona);
256
+ const declared = runnablePersona(personas, persona);
257
+ const current = enabled === undefined || disposition === undefined
258
+ ? await scheduleStore.get(persona)
259
+ : null;
260
+ const created = disposition === undefined && !current ? declared.disposition : undefined;
261
+ if (enabled ?? current?.enabled) {
262
+ refuseInProduction(persona, disposition ?? current?.disposition ?? created ?? 'realistic', config, environments, environment);
263
+ }
247
264
  return scheduleStore.set({
248
265
  persona,
249
266
  enabled,
250
- disposition: disposition,
267
+ disposition: (disposition ?? created),
251
268
  goals,
252
269
  budget: budget === undefined
253
270
  ? undefined
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pikku/core",
3
- "version": "0.12.121",
3
+ "version": "0.12.122",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/pikkujs/pikku.git",