@kensio/yulin 1.21.32 → 1.21.34

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.
@@ -12,6 +12,11 @@ export declare function simSdkWireOutputResponse(output: unknown, contentType?:
12
12
  * AWS exception name and the status real AWS answers with, so a handler
13
13
  * catching `ResourceNotFoundException` catches the same thing here as in
14
14
  * production.
15
+ *
16
+ * The body also carries the exception's modelled members, such as the
17
+ * `CancellationReasons` of a DynamoDB `TransactionCanceledException`. The SDK
18
+ * maps those from the body onto the exception it throws, so a member left out
19
+ * here is undefined on the caller's side.
15
20
  */
16
21
  export declare function simSdkWireErrorResponse(error: unknown, contentType?: string): SimSdkWireResponse;
17
22
  /**
@@ -24,6 +24,11 @@ export function simSdkWireOutputResponse(output, contentType = defaultContentTyp
24
24
  * AWS exception name and the status real AWS answers with, so a handler
25
25
  * catching `ResourceNotFoundException` catches the same thing here as in
26
26
  * production.
27
+ *
28
+ * The body also carries the exception's modelled members, such as the
29
+ * `CancellationReasons` of a DynamoDB `TransactionCanceledException`. The SDK
30
+ * maps those from the body onto the exception it throws, so a member left out
31
+ * here is undefined on the caller's side.
27
32
  */
28
33
  export function simSdkWireErrorResponse(error, contentType = defaultContentType) {
29
34
  const errorType = error instanceof Error ? error.name : "InternalFailure";
@@ -35,11 +40,26 @@ export function simSdkWireErrorResponse(error, contentType = defaultContentType)
35
40
  ...Object.fromEntries([["x-amzn-errortype", errorType]]),
36
41
  },
37
42
  body: simSdkWireJsonBody(Object.fromEntries([
43
+ ...errorMembers(error),
38
44
  ["__type", errorType],
39
45
  ["message", message],
40
46
  ])),
41
47
  };
42
48
  }
49
+ /**
50
+ * The modelled members a simulated service error carries.
51
+ *
52
+ * AWS names the members of its exception shapes in PascalCase, as it names
53
+ * the members of every other shape. The lower-case properties of an Error are
54
+ * either JavaScript's own or the simulator's diagnostics, such as the
55
+ * principal an IAM denial names, and none of them is part of the response.
56
+ */
57
+ function errorMembers(error) {
58
+ if (!(error instanceof Error)) {
59
+ return [];
60
+ }
61
+ return Object.entries(error).filter(([name]) => /^[A-Z]/u.test(name));
62
+ }
43
63
  /**
44
64
  * The status a simulated service error is answered with.
45
65
  *
@@ -3,7 +3,7 @@ import { simGlueAtLeast, simGlueAtMost, simGlueColumnValue, simGlueEqual, simGlu
3
3
  export function simGlueComparisonFilter(column, holds, literal) {
4
4
  return (values) => {
5
5
  const placed = simGluePartitionOrder(column, simGlueColumnValue(column, values), literal);
6
- return placed === undefined ? false : holds(placed);
6
+ return placed !== undefined && holds(placed);
7
7
  };
8
8
  }
9
9
  /** A filter holding for a key between two literals, both ends included. */
@@ -13,7 +13,10 @@ function expectsAbsentKey(expected) {
13
13
  if (expected === "true") {
14
14
  return true;
15
15
  }
16
- return expected === "false" ? false : undefined;
16
+ if (expected === "false") {
17
+ return false;
18
+ }
19
+ return undefined;
17
20
  }
18
21
  /**
19
22
  * Whether a request value counts as present for a `Null` check.
@@ -70,6 +70,7 @@ export class CreateFunctionCommandHandler {
70
70
  ...input,
71
71
  code,
72
72
  environment,
73
+ environmentConflicts: this.environmentConflicts,
73
74
  accountRegionScope: this.accountRegionScope,
74
75
  runAsOwner: this.runAsOwner,
75
76
  clock: this.background,
@@ -0,0 +1,22 @@
1
+ import type { SimLambdaEnvironment } from "./sim-lambda-environment.js";
2
+ /**
3
+ * A difference between declared values that matters once the name is read
4
+ * outside an invocation.
5
+ *
6
+ * The key says which kind of difference it is and for which name, and is
7
+ * what a warning is printed once for.
8
+ */
9
+ export interface SimLambdaEnvironmentConflict {
10
+ readonly key: string;
11
+ readonly name: string;
12
+ readonly message: string;
13
+ }
14
+ /**
15
+ * The conflict of a function declaring a name the host process also sets,
16
+ * with a different value.
17
+ */
18
+ export declare function simLambdaHostConflict(environment: SimLambdaEnvironment, name: string): SimLambdaEnvironmentConflict;
19
+ /**
20
+ * The conflict of two functions declaring different values for one name.
21
+ */
22
+ export declare function simLambdaFunctionConflict(existing: SimLambdaEnvironment, environment: SimLambdaEnvironment, name: string): SimLambdaEnvironmentConflict;
@@ -0,0 +1,33 @@
1
+ /**
2
+ * The advice that follows each warning, given the name that was read early.
3
+ */
4
+ function moveTheReadAdvice(name) {
5
+ return (`Code read process.env.${name} outside any invocation, as a read at ` +
6
+ "module scope does when the module is imported, and saw the host value " +
7
+ "instead. Move the read inside the handler function.");
8
+ }
9
+ /**
10
+ * The conflict of a function declaring a name the host process also sets,
11
+ * with a different value.
12
+ */
13
+ export function simLambdaHostConflict(environment, name) {
14
+ return {
15
+ key: `host:${name}`,
16
+ name,
17
+ message: `Sim Lambda function "${environment.functionName}" declares ` +
18
+ `environment variable ${name}, which the host process also sets ` +
19
+ `with a different value. ${moveTheReadAdvice(name)}`,
20
+ };
21
+ }
22
+ /**
23
+ * The conflict of two functions declaring different values for one name.
24
+ */
25
+ export function simLambdaFunctionConflict(existing, environment, name) {
26
+ return {
27
+ key: `function:${name}`,
28
+ name,
29
+ message: `Sim Lambda functions "${existing.functionName}" and ` +
30
+ `"${environment.functionName}" declare different values for ` +
31
+ `environment variable ${name}. ${moveTheReadAdvice(name)}`,
32
+ };
33
+ }
@@ -1,15 +1,32 @@
1
1
  import type { SimLambdaEnvironment } from "./sim-lambda-environment.js";
2
+ interface SimLambdaEnvironmentConflictsProperties {
3
+ /**
4
+ * The environments of the functions the simulated Lambda holds when it is
5
+ * asked, which a held name is checked against again once it is read. A
6
+ * check built standalone has none.
7
+ */
8
+ readonly currentEnvironments?: () => Iterable<SimLambdaEnvironment>;
9
+ }
2
10
  /**
3
11
  * Warns about declared environment variables whose value a handler module
4
- * might not actually see.
12
+ * did not actually see.
5
13
  *
6
14
  * A sim Lambda function backed by a real in-process handler only gets its own
7
15
  * process.env while the handler runs, so a variable read at module scope is
8
16
  * read too early (see SimLambdaProcessEnv). That only changes what the code
9
- * sees when the value read at module scope differs from the declared one, so
10
- * these are the two conditions worth reporting: the host process sets the
11
- * same name with a different value, or another simulated function declares
12
- * the same name with a different value.
17
+ * sees when the value read at module scope differs from the declared one.
18
+ * There are two ways for it to differ. The host process sets the same name
19
+ * with a different value, or another simulated function declares the same
20
+ * name with a different value.
21
+ *
22
+ * A difference is reported only once code has read the name from process.env
23
+ * outside any invocation, which SimProcessEnvironment records. The name of a
24
+ * difference with no such read yet is held. If the read turns up before a
25
+ * later invocation (a handler module imported after the function was
26
+ * created), the name is checked again against the functions that exist then,
27
+ * so a function deleted or reconfigured in between leaves nothing stale to
28
+ * report. A read made before Yulin was imported is not on record, and leaves
29
+ * the difference unreported.
13
30
  *
14
31
  * Staying quiet when the values agree matters as much as warning when they do
15
32
  * not. Test suites commonly export the same variables they configure their
@@ -18,30 +35,55 @@ import type { SimLambdaEnvironment } from "./sim-lambda-environment.js";
18
35
  */
19
36
  export declare class SimLambdaEnvironmentConflicts {
20
37
  /**
21
- * Variable names already reported, so repeated function creation does not
38
+ * The environments of the functions that exist now.
39
+ */
40
+ private readonly currentEnvironments;
41
+ /**
42
+ * Conflict keys already reported, so repeated function creation does not
22
43
  * repeat the same warning.
23
44
  */
24
45
  private readonly warned;
46
+ /**
47
+ * Names in conflict that had not been read outside an invocation when the
48
+ * conflict was found.
49
+ */
50
+ private readonly held;
51
+ constructor(properties?: SimLambdaEnvironmentConflictsProperties);
25
52
  /**
26
53
  * Check a new function's environment against the host process and against
27
54
  * the functions that already exist.
28
55
  */
29
56
  check(environment: SimLambdaEnvironment, existingEnvironments: Iterable<SimLambdaEnvironment>): void;
30
57
  /**
31
- * Warn when the host process sets the same name with a different value.
58
+ * Check the held names that have since been read outside an invocation
59
+ * again, against the functions that exist now, and report what conflicts.
60
+ *
61
+ * Called as a function is invoked, before the handler runs.
32
62
  */
33
- private checkHostProcess;
63
+ reportReads(): void;
64
+ /**
65
+ * Check one name across the given environments, each against the host
66
+ * process and against the environments before it.
67
+ */
68
+ private checkName;
34
69
  /**
35
- * Warn when another simulated function declares the same name with a
70
+ * Find a conflict when the host process sets the same name with a
36
71
  * different value.
37
72
  */
73
+ private checkHostProcess;
74
+ /**
75
+ * Find a conflict when another simulated function declares the same name
76
+ * with a different value.
77
+ */
38
78
  private checkOtherFunctions;
39
79
  /**
40
- * Report a conflict once per variable name.
80
+ * Warn about a conflict once per key if its name has been read outside an
81
+ * invocation, and hold the name otherwise.
41
82
  *
42
83
  * Warnings go to the console because that is where a test runner surfaces
43
84
  * them next to the failing expectation they explain; the simulator has no
44
85
  * logger of its own to route them through.
45
86
  */
46
- private warn;
87
+ private report;
47
88
  }
89
+ export {};
@@ -1,18 +1,25 @@
1
+ import { simLambdaFunctionConflict, simLambdaHostConflict, } from "./sim-lambda-environment-conflict.js";
1
2
  import { simProcessEnvironment } from "../../../../util/process/sim-process-environment.js";
2
- const moveTheReadAdvice = "A read of process.env at module scope happens when the module is " +
3
- "imported, before any invocation, so it sees the host value instead. " +
4
- "Move the read inside the handler function.";
5
3
  /**
6
4
  * Warns about declared environment variables whose value a handler module
7
- * might not actually see.
5
+ * did not actually see.
8
6
  *
9
7
  * A sim Lambda function backed by a real in-process handler only gets its own
10
8
  * process.env while the handler runs, so a variable read at module scope is
11
9
  * read too early (see SimLambdaProcessEnv). That only changes what the code
12
- * sees when the value read at module scope differs from the declared one, so
13
- * these are the two conditions worth reporting: the host process sets the
14
- * same name with a different value, or another simulated function declares
15
- * the same name with a different value.
10
+ * sees when the value read at module scope differs from the declared one.
11
+ * There are two ways for it to differ. The host process sets the same name
12
+ * with a different value, or another simulated function declares the same
13
+ * name with a different value.
14
+ *
15
+ * A difference is reported only once code has read the name from process.env
16
+ * outside any invocation, which SimProcessEnvironment records. The name of a
17
+ * difference with no such read yet is held. If the read turns up before a
18
+ * later invocation (a handler module imported after the function was
19
+ * created), the name is checked again against the functions that exist then,
20
+ * so a function deleted or reconfigured in between leaves nothing stale to
21
+ * report. A read made before Yulin was imported is not on record, and leaves
22
+ * the difference unreported.
16
23
  *
17
24
  * Staying quiet when the values agree matters as much as warning when they do
18
25
  * not. Test suites commonly export the same variables they configure their
@@ -21,22 +28,68 @@ const moveTheReadAdvice = "A read of process.env at module scope happens when th
21
28
  */
22
29
  export class SimLambdaEnvironmentConflicts {
23
30
  /**
24
- * Variable names already reported, so repeated function creation does not
31
+ * The environments of the functions that exist now.
32
+ */
33
+ currentEnvironments;
34
+ /**
35
+ * Conflict keys already reported, so repeated function creation does not
25
36
  * repeat the same warning.
26
37
  */
27
38
  warned = new Set();
39
+ /**
40
+ * Names in conflict that had not been read outside an invocation when the
41
+ * conflict was found.
42
+ */
43
+ held = new Set();
44
+ constructor(properties = {}) {
45
+ this.currentEnvironments =
46
+ properties.currentEnvironments ?? (() => []);
47
+ }
28
48
  /**
29
49
  * Check a new function's environment against the host process and against
30
50
  * the functions that already exist.
31
51
  */
32
52
  check(environment, existingEnvironments) {
53
+ const existing = [...existingEnvironments];
33
54
  for (const [name, value] of environment.declaredVariables) {
34
55
  this.checkHostProcess(environment, name, value);
35
- this.checkOtherFunctions(environment, name, value, existingEnvironments);
56
+ this.checkOtherFunctions(environment, name, value, existing);
57
+ }
58
+ }
59
+ /**
60
+ * Check the held names that have since been read outside an invocation
61
+ * again, against the functions that exist now, and report what conflicts.
62
+ *
63
+ * Called as a function is invoked, before the handler runs.
64
+ */
65
+ reportReads() {
66
+ const read = [...this.held].filter((name) => simProcessEnvironment.wasReadOutsideRun(name));
67
+ if (read.length === 0) {
68
+ return;
69
+ }
70
+ const environments = [...this.currentEnvironments()];
71
+ for (const name of read) {
72
+ this.held.delete(name);
73
+ this.checkName(name, environments);
74
+ }
75
+ }
76
+ /**
77
+ * Check one name across the given environments, each against the host
78
+ * process and against the environments before it.
79
+ */
80
+ checkName(name, environments) {
81
+ for (const [index, environment] of environments.entries()) {
82
+ const value = environment.declaredVariables.get(name);
83
+ if (value === undefined) {
84
+ continue;
85
+ }
86
+ this.checkHostProcess(environment, name, value);
87
+ this.checkOtherFunctions(environment, name, value, environments.slice(0, index));
36
88
  }
37
89
  }
38
90
  /**
39
- * Warn when the host process sets the same name with a different value.
91
+ * Find a conflict when the host process sets the same name with a
92
+ * different value.
40
93
  */
41
94
  checkHostProcess(environment, name, value) {
42
95
  // Read-only lookup of a name the function itself declared.
@@ -45,13 +98,11 @@ export class SimLambdaEnvironmentConflicts {
45
98
  if (hostValue === undefined || hostValue === value) {
46
99
  return;
47
100
  }
48
- this.warn(`host:${name}`, `Sim Lambda function "${environment.functionName}" declares ` +
49
- `environment variable ${name}, which the host process also sets ` +
50
- `with a different value. ${moveTheReadAdvice}`);
101
+ this.report(simLambdaHostConflict(environment, name));
51
102
  }
52
103
  /**
53
- * Warn when another simulated function declares the same name with a
54
- * different value.
104
+ * Find a conflict when another simulated function declares the same name
105
+ * with a different value.
55
106
  */
56
107
  checkOtherFunctions(environment, name, value, existingEnvironments) {
57
108
  for (const existing of existingEnvironments) {
@@ -59,25 +110,28 @@ export class SimLambdaEnvironmentConflicts {
59
110
  if (existingValue === undefined || existingValue === value) {
60
111
  continue;
61
112
  }
62
- this.warn(`function:${name}`, `Sim Lambda functions "${existing.functionName}" and ` +
63
- `"${environment.functionName}" declare different values for ` +
64
- `environment variable ${name}. ${moveTheReadAdvice}`);
113
+ this.report(simLambdaFunctionConflict(existing, environment, name));
65
114
  return;
66
115
  }
67
116
  }
68
117
  /**
69
- * Report a conflict once per variable name.
118
+ * Warn about a conflict once per key if its name has been read outside an
119
+ * invocation, and hold the name otherwise.
70
120
  *
71
121
  * Warnings go to the console because that is where a test runner surfaces
72
122
  * them next to the failing expectation they explain; the simulator has no
73
123
  * logger of its own to route them through.
74
124
  */
75
- warn(key, message) {
76
- if (this.warned.has(key)) {
125
+ report(conflict) {
126
+ if (this.warned.has(conflict.key)) {
127
+ return;
128
+ }
129
+ if (!simProcessEnvironment.wasReadOutsideRun(conflict.name)) {
130
+ this.held.add(conflict.name);
77
131
  return;
78
132
  }
79
- this.warned.add(key);
133
+ this.warned.add(conflict.key);
80
134
  // oxlint-disable-next-line no-console
81
- console.warn(message);
135
+ console.warn(conflict.message);
82
136
  }
83
137
  }
@@ -194,6 +194,7 @@ export class SimLambdaFunction {
194
194
  logStreamName: this.logging.logStreamName(),
195
195
  });
196
196
  this.logging.recordFrom(this.#code);
197
+ this.properties.environmentConflicts?.reportReads();
197
198
  return await this.metrics.around(String(this.name), async () => simAwsRunAsContext.run(this.runAsOwner, { kind: "arn", arn: this.roleArn }, async () => await this.environment.runWith(async () => await this.runInHostScope(async () => await this.logging.around(async () => await invocation.run(this.#code.handlerFunction(), event))))));
198
199
  }
199
200
  /**
@@ -8,6 +8,7 @@ import type { SimLambdaOutput } from "./logging/sim-lambda-output.js";
8
8
  import type { SimLogsServiceWriter } from "../../logs/write/sim-logs-service-writer.js";
9
9
  import type { SimLambdaExecutableCode } from "./code/sim-lambda-executable-code.js";
10
10
  import type { SimLambdaEnvironment } from "./environment/sim-lambda-environment.js";
11
+ import type { SimLambdaEnvironmentConflicts } from "./environment/sim-lambda-environment-conflicts.js";
11
12
  import type { SimLambdaOutboundHttp } from "./outbound/sim-lambda-outbound-http.js";
12
13
  import type { SimLambdaFunctionState } from "./sim-lambda-function-configuration.js";
13
14
  import type { SimLambdaHandler } from "./sim-lambda-handler.type.js";
@@ -27,6 +28,13 @@ export interface SimLambdaFunctionProperties {
27
28
  timeoutSeconds?: number | undefined;
28
29
  memorySizeMb?: number | undefined;
29
30
  environment?: SimLambdaEnvironment | undefined;
31
+ /**
32
+ * The check that warns about declared variables read too early, shared with
33
+ * every other function of the same simulated Lambda. It reports, as this
34
+ * function is invoked, a difference found earlier whose name has since been
35
+ * read outside an invocation. A function built standalone has none.
36
+ */
37
+ environmentConflicts?: SimLambdaEnvironmentConflicts | undefined;
30
38
  /**
31
39
  * The queue or topic this function's abandoned asynchronous events are sent
32
40
  * to, which is what `DeadLetterConfig.TargetArn` names.
@@ -146,7 +146,9 @@ export class SimLambdaCommands {
146
146
  runAsOwner: properties.runAsOwner,
147
147
  // Shared across function creations so a conflicting environment variable
148
148
  // is only reported once for this simulated Lambda.
149
- environmentConflicts: new SimLambdaEnvironmentConflicts(),
149
+ environmentConflicts: new SimLambdaEnvironmentConflicts({
150
+ currentEnvironments: () => properties.functions.values().map((existing) => existing.environment),
151
+ }),
150
152
  eventInvokeConfigs: this.eventInvokeConfigStore,
151
153
  destinations,
152
154
  codeStore,
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Records the names code reads from the host process.env.
3
+ *
4
+ * SimLambdaEnvironmentConflicts warns that a module-scope read sees the host
5
+ * value of a variable instead of the function's own. Whether that matters
6
+ * depends on a read having happened at all, which configuration cannot tell.
7
+ * This keeps the evidence: the host environment is handed out behind a Proxy
8
+ * that notes each name read through it.
9
+ *
10
+ * Enumerating the whole environment is not counted as reading every name in
11
+ * it. Node.js copies process.env key by key when it spawns a child process,
12
+ * and so does any tool that snapshots it, and none of that is handler code
13
+ * capturing a value it later depends on. An enumeration is recognised by its
14
+ * shape: the property reads that follow a listing of the keys, in the order
15
+ * the listing gave them. Its reads are recorded as they happen and taken off
16
+ * the record again once every key has been read, so a listing that is not
17
+ * followed by a complete copy hides nothing. A module copying the environment
18
+ * with a spread at module scope is missed as a result.
19
+ */
20
+ export declare class SimProcessEnvironmentReads {
21
+ private readonly names;
22
+ /**
23
+ * The environment objects each watching Proxy stands in front of, so that
24
+ * assigning one back to process.env does not stack a Proxy on a Proxy.
25
+ */
26
+ private readonly targets;
27
+ /**
28
+ * The keys of an enumeration in progress, and how far its reads have got.
29
+ */
30
+ private enumeration;
31
+ /**
32
+ * Whether code has read the name through a watched environment.
33
+ */
34
+ wasRead(name: string): boolean;
35
+ /**
36
+ * A view of the environment that records the names read through it.
37
+ *
38
+ * Writes and deletes go straight to the environment, so a value assigned
39
+ * through the view is coerced to a string as process.env would coerce it.
40
+ */
41
+ watch(environment: NodeJS.ProcessEnv): NodeJS.ProcessEnv;
42
+ /**
43
+ * The environment behind a watching view, or the environment itself when it
44
+ * is not one.
45
+ */
46
+ unwrap(environment: NodeJS.ProcessEnv): NodeJS.ProcessEnv;
47
+ /**
48
+ * Note a read, and take an enumeration's reads off the record once it has
49
+ * read every key.
50
+ */
51
+ private recordRead;
52
+ }
@@ -0,0 +1,94 @@
1
+ /**
2
+ * Records the names code reads from the host process.env.
3
+ *
4
+ * SimLambdaEnvironmentConflicts warns that a module-scope read sees the host
5
+ * value of a variable instead of the function's own. Whether that matters
6
+ * depends on a read having happened at all, which configuration cannot tell.
7
+ * This keeps the evidence: the host environment is handed out behind a Proxy
8
+ * that notes each name read through it.
9
+ *
10
+ * Enumerating the whole environment is not counted as reading every name in
11
+ * it. Node.js copies process.env key by key when it spawns a child process,
12
+ * and so does any tool that snapshots it, and none of that is handler code
13
+ * capturing a value it later depends on. An enumeration is recognised by its
14
+ * shape: the property reads that follow a listing of the keys, in the order
15
+ * the listing gave them. Its reads are recorded as they happen and taken off
16
+ * the record again once every key has been read, so a listing that is not
17
+ * followed by a complete copy hides nothing. A module copying the environment
18
+ * with a spread at module scope is missed as a result.
19
+ */
20
+ export class SimProcessEnvironmentReads {
21
+ names = new Set();
22
+ /**
23
+ * The environment objects each watching Proxy stands in front of, so that
24
+ * assigning one back to process.env does not stack a Proxy on a Proxy.
25
+ */
26
+ targets = new WeakMap();
27
+ /**
28
+ * The keys of an enumeration in progress, and how far its reads have got.
29
+ */
30
+ enumeration;
31
+ /**
32
+ * Whether code has read the name through a watched environment.
33
+ */
34
+ wasRead(name) {
35
+ return this.names.has(name);
36
+ }
37
+ /**
38
+ * A view of the environment that records the names read through it.
39
+ *
40
+ * Writes and deletes go straight to the environment, so a value assigned
41
+ * through the view is coerced to a string as process.env would coerce it.
42
+ */
43
+ watch(environment) {
44
+ const target = this.unwrap(environment);
45
+ const view = new Proxy(target, {
46
+ get: (watched, key) => {
47
+ if (typeof key === "string") {
48
+ this.recordRead(key);
49
+ }
50
+ return Reflect.get(watched, key);
51
+ },
52
+ set: (watched, key, value) => Reflect.set(watched, key, value),
53
+ ownKeys: (watched) => {
54
+ const keys = Reflect.ownKeys(watched);
55
+ this.enumeration = { keys, next: 0, added: [] };
56
+ return keys;
57
+ },
58
+ });
59
+ this.targets.set(view, target);
60
+ return view;
61
+ }
62
+ /**
63
+ * The environment behind a watching view, or the environment itself when it
64
+ * is not one.
65
+ */
66
+ unwrap(environment) {
67
+ return this.targets.get(environment) ?? environment;
68
+ }
69
+ /**
70
+ * Note a read, and take an enumeration's reads off the record once it has
71
+ * read every key.
72
+ */
73
+ recordRead(name) {
74
+ const enumeration = this.enumeration;
75
+ if (enumeration === undefined ||
76
+ enumeration.keys[enumeration.next] !== name) {
77
+ this.enumeration = undefined;
78
+ this.names.add(name);
79
+ return;
80
+ }
81
+ enumeration.next += 1;
82
+ if (!this.names.has(name)) {
83
+ this.names.add(name);
84
+ enumeration.added.push(name);
85
+ }
86
+ if (enumeration.next < enumeration.keys.length) {
87
+ return;
88
+ }
89
+ for (const added of enumeration.added) {
90
+ this.names.delete(added);
91
+ }
92
+ this.enumeration = undefined;
93
+ }
94
+ }
@@ -20,19 +20,33 @@
20
20
  *
21
21
  * The one thing this cannot reach is a read that already happened. A handler
22
22
  * module doing `const TABLE = process.env.TABLE_NAME` at module scope is
23
- * evaluated when the test file imports it, long before any run, so it captures
23
+ * evaluated when the test file imports it, long before any run, and captures
24
24
  * the host value. Reads inside the handler body see the run's own variables.
25
- * See SimLambdaEnvironmentConflicts, which warns when that distinction
26
- * actually changes what the code sees.
25
+ *
26
+ * Outside a run, process.env is the host environment behind a
27
+ * SimProcessEnvironmentReads view, which records the names read. The patch is
28
+ * installed when this module loads, which is when Yulin is imported, so a
29
+ * module-scope read in a handler imported after Yulin is on record.
30
+ * SimLambdaEnvironmentConflicts warns from that record, and only for a name
31
+ * whose early read changed what the code saw. A read made before Yulin was
32
+ * imported is invisible to it.
27
33
  */
28
34
  declare class SimProcessEnvironment {
29
35
  private readonly storage;
36
+ private readonly reads;
30
37
  private hostEnvironment;
31
- private installed;
38
+ private hostView;
39
+ constructor();
32
40
  /**
33
41
  * Run something with the given variables as its process.env.
34
42
  */
35
43
  run<T>(variables: Record<string, string>, run: () => Promise<T>): Promise<T>;
44
+ /**
45
+ * Whether code read the name from process.env while no run was active.
46
+ *
47
+ * Reads made before this module loaded are not on record.
48
+ */
49
+ wasReadOutsideRun(name: string): boolean;
36
50
  /**
37
51
  * The host process environment, ignoring any run in progress.
38
52
  */
@@ -51,14 +65,23 @@ declare class SimProcessEnvironment {
51
65
  *
52
66
  * process.env is a plain configurable data property on process, so it can
53
67
  * be replaced with a getter. Outside a simulated run the getter hands back
54
- * the untouched host environment object, so the patch is inert: anything
55
- * that reads, writes, or stubs process.env normally is unaffected.
68
+ * the host environment behind a view that records the names read, and
69
+ * passes every read, write and delete through to it. Anything that reads,
70
+ * writes, or stubs process.env normally is unaffected.
56
71
  *
57
- * Installed once and never removed. Removing it would be unsafe while
58
- * another run is in flight, and there is nothing to gain: an installed patch
59
- * with no run in progress behaves exactly like no patch at all.
72
+ * Installed once, at construction, and never removed. Removing it would be
73
+ * unsafe while another run is in flight, and there is nothing to gain. With
74
+ * no run in progress, the patch passes everything through to the host
75
+ * environment.
60
76
  */
61
77
  private install;
78
+ /**
79
+ * Make the environment the host environment, and watch reads of it.
80
+ *
81
+ * Code saving process.env and assigning it back hands over the watching
82
+ * view, which is unwrapped so that views do not nest.
83
+ */
84
+ private useHostEnvironment;
62
85
  /**
63
86
  * Apply an assignment to process.env as a whole.
64
87
  *
@@ -1,4 +1,5 @@
1
1
  import { AsyncLocalStorage } from "node:async_hooks";
2
+ import { SimProcessEnvironmentReads } from "./sim-process-environment-reads.js";
2
3
  /**
3
4
  * The process.env in-process handler code sees while a simulated run lasts.
4
5
  *
@@ -21,22 +22,39 @@ import { AsyncLocalStorage } from "node:async_hooks";
21
22
  *
22
23
  * The one thing this cannot reach is a read that already happened. A handler
23
24
  * module doing `const TABLE = process.env.TABLE_NAME` at module scope is
24
- * evaluated when the test file imports it, long before any run, so it captures
25
+ * evaluated when the test file imports it, long before any run, and captures
25
26
  * the host value. Reads inside the handler body see the run's own variables.
26
- * See SimLambdaEnvironmentConflicts, which warns when that distinction
27
- * actually changes what the code sees.
27
+ *
28
+ * Outside a run, process.env is the host environment behind a
29
+ * SimProcessEnvironmentReads view, which records the names read. The patch is
30
+ * installed when this module loads, which is when Yulin is imported, so a
31
+ * module-scope read in a handler imported after Yulin is on record.
32
+ * SimLambdaEnvironmentConflicts warns from that record, and only for a name
33
+ * whose early read changed what the code saw. A read made before Yulin was
34
+ * imported is invisible to it.
28
35
  */
29
36
  class SimProcessEnvironment {
30
37
  storage = new AsyncLocalStorage();
38
+ reads = new SimProcessEnvironmentReads();
31
39
  hostEnvironment = process.env;
32
- installed = false;
40
+ hostView = process.env;
41
+ constructor() {
42
+ this.install();
43
+ }
33
44
  /**
34
45
  * Run something with the given variables as its process.env.
35
46
  */
36
47
  async run(variables, run) {
37
- this.install();
38
48
  return await this.storage.run(variables, run);
39
49
  }
50
+ /**
51
+ * Whether code read the name from process.env while no run was active.
52
+ *
53
+ * Reads made before this module loaded are not on record.
54
+ */
55
+ wasReadOutsideRun(name) {
56
+ return this.reads.wasRead(name);
57
+ }
40
58
  /**
41
59
  * The host process environment, ignoring any run in progress.
42
60
  */
@@ -65,27 +83,35 @@ class SimProcessEnvironment {
65
83
  *
66
84
  * process.env is a plain configurable data property on process, so it can
67
85
  * be replaced with a getter. Outside a simulated run the getter hands back
68
- * the untouched host environment object, so the patch is inert: anything
69
- * that reads, writes, or stubs process.env normally is unaffected.
86
+ * the host environment behind a view that records the names read, and
87
+ * passes every read, write and delete through to it. Anything that reads,
88
+ * writes, or stubs process.env normally is unaffected.
70
89
  *
71
- * Installed once and never removed. Removing it would be unsafe while
72
- * another run is in flight, and there is nothing to gain: an installed patch
73
- * with no run in progress behaves exactly like no patch at all.
90
+ * Installed once, at construction, and never removed. Removing it would be
91
+ * unsafe while another run is in flight, and there is nothing to gain. With
92
+ * no run in progress, the patch passes everything through to the host
93
+ * environment.
74
94
  */
75
95
  install() {
76
- if (this.installed) {
77
- return;
78
- }
79
- this.hostEnvironment = process.env;
96
+ this.useHostEnvironment(process.env);
80
97
  Object.defineProperty(process, "env", {
81
98
  configurable: true,
82
99
  enumerable: true,
83
- get: () => this.storage.getStore() ?? this.hostEnvironment,
100
+ get: () => this.storage.getStore() ?? this.hostView,
84
101
  set: (replacement) => {
85
102
  this.replaceVariables(replacement);
86
103
  },
87
104
  });
88
- this.installed = true;
105
+ }
106
+ /**
107
+ * Make the environment the host environment, and watch reads of it.
108
+ *
109
+ * Code saving process.env and assigning it back hands over the watching
110
+ * view, which is unwrapped so that views do not nest.
111
+ */
112
+ useHostEnvironment(environment) {
113
+ this.hostEnvironment = this.reads.unwrap(environment);
114
+ this.hostView = this.reads.watch(this.hostEnvironment);
89
115
  }
90
116
  /**
91
117
  * Apply an assignment to process.env as a whole.
@@ -104,7 +130,7 @@ class SimProcessEnvironment {
104
130
  replaceVariables(replacement) {
105
131
  const store = this.storage.getStore();
106
132
  if (store === undefined) {
107
- this.hostEnvironment = replacement;
133
+ this.useHostEnvironment(replacement);
108
134
  return;
109
135
  }
110
136
  const replacing = Object.entries(replacement);
@@ -3355,6 +3355,12 @@ console in the two situations where the difference changes what your code sees:
3355
3355
  - a declared variable whose name the host process also sets, with a different value
3356
3356
  - two simulated functions declaring the same variable name with different values
3357
3357
 
3358
+ Either one is reported only once code has read that name from `process.env` with no invocation
3359
+ running. Yulin records those reads from the moment it is imported. A read made earlier is invisible
3360
+ to it (a handler module imported ahead of Yulin, or a setup file that loads first), and so is a copy
3361
+ of the whole environment such as `{ ...process.env }`. Neither produces a warning. A handler that
3362
+ reads its variables inside the handler function produces none either.
3363
+
3358
3364
  ## The time inside a handler
3359
3365
 
3360
3366
  A function runs on its simulation's clock, and so does the function code. `Date.now()` and
@@ -3317,6 +3317,22 @@ The `getBucketWebsiteUrl(...)` method returns the simulated S3 website URL for t
3317
3317
  `localUrl(...)` method on the localhost server adapts that URL so the request is sent to the local
3318
3318
  server while preserving the simulated S3 website hostname.
3319
3319
 
3320
+ ### AWS CLI uploads
3321
+
3322
+ Set `preferred_transfer_client` to `classic` in the AWS config file used for the simulated
3323
+ endpoint:
3324
+
3325
+ ```ini
3326
+ [default]
3327
+ s3 =
3328
+ preferred_transfer_client = classic
3329
+ ```
3330
+
3331
+ The CLI's [automatic transfer-client selection](https://docs.aws.amazon.com/cli/latest/topic/s3-config.html#preferred-transfer-client)
3332
+ can choose CRT. Its uploads use streaming payload trailers, which Yulin's signature verifier
3333
+ does not support. Those requests fail with `403`. The classic client supports the single-file
3334
+ and multipart uploads exercised by Yulin's CLI tests.
3335
+
3320
3336
  ## Presigned URLs
3321
3337
 
3322
3338
  Sim S3 serves a REST API endpoint alongside the website endpoint, and it accepts presigned URLs
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kensio/yulin",
3
- "version": "1.21.32",
3
+ "version": "1.21.34",
4
4
  "description": "AWS system behaviour simulation for isolated unit testing",
5
5
  "repository": "https://github.com/KensioSoftware/yulin",
6
6
  "homepage": "https://yulinsim.dev/",
@@ -293,13 +293,13 @@
293
293
  "eslint-plugin-jsdoc": "^64.0.1",
294
294
  "eslint-plugin-no-secrets": "^2.3.3",
295
295
  "eslint-plugin-security": "^4.0.1",
296
- "eslint-plugin-unicorn": "^75.0.0",
296
+ "eslint-plugin-unicorn": "^76.0.0",
297
297
  "execa": "^10.0.0",
298
298
  "fta-cli": "^3.0.0",
299
299
  "hyparquet": "^1.29.2",
300
300
  "hyparquet-writer": "^0.16.9",
301
301
  "node-sql-parser": "^5.4.0",
302
- "oxfmt": "^0.72.0",
302
+ "oxfmt": "^0.73.0",
303
303
  "oxlint": "^1.77.0",
304
304
  "oxlint-tsgolint": "^7.0.2001",
305
305
  "semantic-release": "25.0.9",