@flighthq/entity 0.3.0-next.906.07cea63 → 0.3.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.
@@ -1,6 +1,7 @@
1
1
  export * from './binding';
2
2
  export * from './clone';
3
3
  export * from './entity';
4
+ export * from './enableEntityRuntimeGuards';
4
5
  export * from './guards';
5
6
  export * from './runtime';
6
7
  //# sourceMappingURL=contract.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"contract.d.ts","sourceRoot":"","sources":["../src/contract.ts"],"names":[],"mappings":"AAAA,cAAc,WAAW,CAAC;AAC1B,cAAc,SAAS,CAAC;AACxB,cAAc,UAAU,CAAC;AACzB,cAAc,UAAU,CAAC;AACzB,cAAc,WAAW,CAAC"}
1
+ {"version":3,"file":"contract.d.ts","sourceRoot":"","sources":["../src/contract.ts"],"names":[],"mappings":"AAAA,cAAc,WAAW,CAAC;AAC1B,cAAc,SAAS,CAAC;AACxB,cAAc,UAAU,CAAC;AACzB,cAAc,6BAA6B,CAAC;AAC5C,cAAc,UAAU,CAAC;AACzB,cAAc,WAAW,CAAC"}
package/dist/contract.js CHANGED
@@ -1,6 +1,7 @@
1
1
  export * from './binding';
2
2
  export * from './clone';
3
3
  export * from './entity';
4
+ export * from './enableEntityRuntimeGuards';
4
5
  export * from './guards';
5
6
  export * from './runtime';
6
7
  //# sourceMappingURL=contract.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"contract.js","sourceRoot":"","sources":["../src/contract.ts"],"names":[],"mappings":"AAAA,cAAc,WAAW,CAAC;AAC1B,cAAc,SAAS,CAAC;AACxB,cAAc,UAAU,CAAC;AACzB,cAAc,UAAU,CAAC;AACzB,cAAc,WAAW,CAAC"}
1
+ {"version":3,"file":"contract.js","sourceRoot":"","sources":["../src/contract.ts"],"names":[],"mappings":"AAAA,cAAc,WAAW,CAAC;AAC1B,cAAc,SAAS,CAAC;AACxB,cAAc,UAAU,CAAC;AACzB,cAAc,6BAA6B,CAAC;AAC5C,cAAc,UAAU,CAAC;AACzB,cAAc,WAAW,CAAC"}
@@ -0,0 +1,3 @@
1
+ export declare function disableEntityRuntimeGuards(): void;
2
+ export declare function enableEntityRuntimeGuards(): void;
3
+ //# sourceMappingURL=enableEntityRuntimeGuards.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"enableEntityRuntimeGuards.d.ts","sourceRoot":"","sources":["../src/enableEntityRuntimeGuards.ts"],"names":[],"mappings":"AAOA,wBAAgB,0BAA0B,IAAI,IAAI,CAGjD;AAWD,wBAAgB,yBAAyB,IAAI,IAAI,CAGhD"}
@@ -0,0 +1,33 @@
1
+ import { logOnce } from '@flighthq/log/contract';
2
+ import { LogLevel } from '@flighthq/types/contract';
3
+ import { setEntityRuntimeGuardMode, setEntityRuntimeWriteGuard } from './guards';
4
+ // Uninstalls the guard installed by enableEntityRuntimeGuards, restoring unguarded entities.
5
+ export function disableEntityRuntimeGuards() {
6
+ setEntityRuntimeGuardMode(false);
7
+ setEntityRuntimeWriteGuard(null);
8
+ }
9
+ // Opt-in development guard mode. When enabled, a direct write to an entity's runtime slot — or to an
10
+ // EntityRuntime's binding slot — that bypasses ensureEntityRuntime / attachEntityBinding is reported,
11
+ // making "the write landed on the wrong entity" and raw-slot-poke bugs visible early. The write is still
12
+ // allowed: the guard observes, it does not block.
13
+ //
14
+ // @flighthq/entity is a CORE package, and core may otherwise depend only on types/core. This module is the
15
+ // sanctioned exception: it is separately importable and shakeable, so a build that never imports it never
16
+ // pulls @flighthq/log, and the layer rule's real concern — feature weight in core's always-loaded graph —
17
+ // does not arise. `packages:check` enforces that the import appears in guard modules only. Idempotent.
18
+ export function enableEntityRuntimeGuards() {
19
+ setEntityRuntimeGuardMode(true);
20
+ setEntityRuntimeWriteGuard(warnOnDirectWrite);
21
+ }
22
+ function warnOnDirectWrite(slot) {
23
+ if (slot === 'binding-slot') {
24
+ logOnce('entity:direct-binding-write', LogLevel.Warn, {
25
+ message: 'EntityRuntime.binding was written directly. Use attachEntityBinding or detachEntityBinding, which keep the binding and the runtime consistent; the write was allowed but is not tracked.',
26
+ }, 'entity');
27
+ return;
28
+ }
29
+ logOnce('entity:direct-runtime-write', LogLevel.Warn, {
30
+ message: "An entity's runtime slot was written directly. Use ensureEntityRuntime or attachEntityBinding; the write was allowed, but bypassing them is how a runtime ends up on the wrong entity.",
31
+ }, 'entity');
32
+ }
33
+ //# sourceMappingURL=enableEntityRuntimeGuards.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"enableEntityRuntimeGuards.js","sourceRoot":"","sources":["../src/enableEntityRuntimeGuards.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,wBAAwB,CAAC;AAEjD,OAAO,EAAE,QAAQ,EAAE,MAAM,0BAA0B,CAAC;AAEpD,OAAO,EAAE,yBAAyB,EAAE,0BAA0B,EAAE,MAAM,UAAU,CAAC;AAEjF,6FAA6F;AAC7F,MAAM,UAAU,0BAA0B;IACxC,yBAAyB,CAAC,KAAK,CAAC,CAAC;IACjC,0BAA0B,CAAC,IAAI,CAAC,CAAC;AACnC,CAAC;AAED,qGAAqG;AACrG,sGAAsG;AACtG,yGAAyG;AACzG,kDAAkD;AAClD,EAAE;AACF,2GAA2G;AAC3G,0GAA0G;AAC1G,0GAA0G;AAC1G,uGAAuG;AACvG,MAAM,UAAU,yBAAyB;IACvC,yBAAyB,CAAC,IAAI,CAAC,CAAC;IAChC,0BAA0B,CAAC,iBAAiB,CAAC,CAAC;AAChD,CAAC;AAED,SAAS,iBAAiB,CAAC,IAA4B;IACrD,IAAI,IAAI,KAAK,cAAc,EAAE,CAAC;QAC5B,OAAO,CACL,6BAA6B,EAC7B,QAAQ,CAAC,IAAI,EACb;YACE,OAAO,EACL,0LAA0L;SAC7L,EACD,QAAQ,CACT,CAAC;QACF,OAAO;IACT,CAAC;IACD,OAAO,CACL,6BAA6B,EAC7B,QAAQ,CAAC,IAAI,EACb;QACE,OAAO,EACL,wLAAwL;KAC3L,EACD,QAAQ,CACT,CAAC;AACJ,CAAC"}
package/dist/guards.d.ts CHANGED
@@ -1,6 +1,7 @@
1
- import type { Entity, EntityRuntime } from '@flighthq/types/contract';
1
+ import type { Entity, EntityRuntime, EntityRuntimeWriteGuard } from '@flighthq/types/contract';
2
2
  export declare function areEntityRuntimeGuardsEnabled(): boolean;
3
3
  export declare function createGuardedEntity<Type extends object>(entity: Type & Entity): Type & Entity;
4
4
  export declare function createGuardedEntityRuntime(runtime: EntityRuntime): EntityRuntime;
5
- export declare function enableEntityRuntimeGuards(): void;
5
+ export declare function setEntityRuntimeGuardMode(enabled: boolean): void;
6
+ export declare function setEntityRuntimeWriteGuard(guard: EntityRuntimeWriteGuard | null): void;
6
7
  //# sourceMappingURL=guards.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"guards.d.ts","sourceRoot":"","sources":["../src/guards.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,0BAA0B,CAAC;AAItE,wBAAgB,6BAA6B,IAAI,OAAO,CAEvD;AAKD,wBAAgB,mBAAmB,CAAC,IAAI,SAAS,MAAM,EAAE,MAAM,EAAE,IAAI,GAAG,MAAM,GAAG,IAAI,GAAG,MAAM,CAmB7F;AAKD,wBAAgB,0BAA0B,CAAC,OAAO,EAAE,aAAa,GAAG,aAAa,CAehF;AASD,wBAAgB,yBAAyB,IAAI,IAAI,CAGhD"}
1
+ {"version":3,"file":"guards.d.ts","sourceRoot":"","sources":["../src/guards.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,aAAa,EAAE,uBAAuB,EAAE,MAAM,0BAA0B,CAAC;AAI/F,wBAAgB,6BAA6B,IAAI,OAAO,CAEvD;AAKD,wBAAgB,mBAAmB,CAAC,IAAI,SAAS,MAAM,EAAE,MAAM,EAAE,IAAI,GAAG,MAAM,GAAG,IAAI,GAAG,MAAM,CAe7F;AAKD,wBAAgB,0BAA0B,CAAC,OAAO,EAAE,aAAa,GAAG,aAAa,CAWhF;AAID,wBAAgB,yBAAyB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAGhE;AAID,wBAAgB,0BAA0B,CAAC,KAAK,EAAE,uBAAuB,GAAG,IAAI,GAAG,IAAI,CAEtF"}
package/dist/guards.js CHANGED
@@ -12,12 +12,11 @@ export function createGuardedEntity(entity) {
12
12
  return new Proxy(entity, {
13
13
  set(target, prop, value) {
14
14
  if (prop === EntityRuntimeKey && _guardsEnabled) {
15
- // Allow writes from trusted paths by checking the stack. Since we cannot reliably
16
- // inspect the stack in all environments, we emit a warning and allow the write.
17
- // In practice, ensureEntityRuntime and attachEntityBinding are the only callers that
18
- // should write to this slot.
19
- // eslint-disable-next-line no-console
20
- console.warn('[entity] Direct write to EntityRuntimeKey detected. Use ensureEntityRuntime or attachEntityBinding instead.', entity);
15
+ // The write is reported and then ALLOWED: the stack cannot be inspected reliably across
16
+ // environments, so trusted callers (ensureEntityRuntime, attachEntityBinding) cannot be told apart
17
+ // from a raw poke. Reporting goes through the seam rather than the console so it uses the standard
18
+ // sink see enableEntityRuntimeGuards.
19
+ _writeGuard?.('runtime-slot');
21
20
  }
22
21
  target[prop] = value;
23
22
  return true;
@@ -33,25 +32,25 @@ export function createGuardedEntityRuntime(runtime) {
33
32
  return new Proxy(runtime, {
34
33
  set(target, prop, value) {
35
34
  if (prop === 'binding' && _guardsEnabled) {
36
- // eslint-disable-next-line no-console
37
- console.warn('[entity] Direct write to EntityRuntime.binding detected. Use attachEntityBinding or detachEntityBinding instead.', runtime);
35
+ _writeGuard?.('binding-slot');
38
36
  }
39
37
  target[prop] = value;
40
38
  return true;
41
39
  },
42
40
  });
43
41
  }
44
- // Opt-in development guard mode. When enabled, direct writes to the EntityRuntimeKey slot
45
- // that bypass ensureEntityRuntime / attachEntityBinding will warn in the console, making
46
- // "writes landed on the wrong entity or raw slot poke" bugs visible early.
47
- //
48
- // This function is a no-op in production if the calling module is never imported — it is
49
- // fully tree-shakable, never called at module top level, and has no effect on
50
- // "sideEffects": false.
51
- export function enableEntityRuntimeGuards() {
52
- if (typeof Proxy === 'undefined')
42
+ // Turns the guarding PROXIES on or off. This is the machinery seam, not the caller-facing entry point —
43
+ // use enableEntityRuntimeGuards, which switches this on and installs the reporter together.
44
+ export function setEntityRuntimeGuardMode(enabled) {
45
+ if (enabled && typeof Proxy === 'undefined')
53
46
  return;
54
- _guardsEnabled = true;
47
+ _guardsEnabled = enabled;
48
+ }
49
+ // The diagnostics seam for a direct slot write. Null uninstalls it, and null is what a build that never
50
+ // imports the guard module sees — which is why this file needs no logger and stays inside the core layer.
51
+ export function setEntityRuntimeWriteGuard(guard) {
52
+ _writeGuard = guard;
55
53
  }
56
54
  let _guardsEnabled = false;
55
+ let _writeGuard = null;
57
56
  //# sourceMappingURL=guards.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"guards.js","sourceRoot":"","sources":["../src/guards.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,gBAAgB,EAAE,MAAM,0BAA0B,CAAC;AAE5D,yFAAyF;AACzF,MAAM,UAAU,6BAA6B;IAC3C,OAAO,cAAc,CAAC;AACxB,CAAC;AAED,uFAAuF;AACvF,uFAAuF;AACvF,yCAAyC;AACzC,MAAM,UAAU,mBAAmB,CAAsB,MAAqB;IAC5E,IAAI,CAAC,cAAc,IAAI,OAAO,KAAK,KAAK,WAAW;QAAE,OAAO,MAAM,CAAC;IACnE,OAAO,IAAI,KAAK,CAAC,MAAM,EAAE;QACvB,GAAG,CAAC,MAAM,EAAE,IAAI,EAAE,KAAK;YACrB,IAAI,IAAI,KAAK,gBAAgB,IAAI,cAAc,EAAE,CAAC;gBAChD,kFAAkF;gBAClF,gFAAgF;gBAChF,qFAAqF;gBACrF,6BAA6B;gBAC7B,sCAAsC;gBACtC,OAAO,CAAC,IAAI,CACV,6GAA6G,EAC7G,MAAM,CACP,CAAC;YACJ,CAAC;YACA,MAAkD,CAAC,IAAI,CAAC,GAAG,KAAK,CAAC;YAClE,OAAO,IAAI,CAAC;QACd,CAAC;KACF,CAAC,CAAC;AACL,CAAC;AAED,0FAA0F;AAC1F,2FAA2F;AAC3F,eAAe;AACf,MAAM,UAAU,0BAA0B,CAAC,OAAsB;IAC/D,IAAI,CAAC,cAAc,IAAI,OAAO,KAAK,KAAK,WAAW;QAAE,OAAO,OAAO,CAAC;IACpE,OAAO,IAAI,KAAK,CAAC,OAAO,EAAE;QACxB,GAAG,CAAC,MAAM,EAAE,IAAI,EAAE,KAAK;YACrB,IAAI,IAAI,KAAK,SAAS,IAAI,cAAc,EAAE,CAAC;gBACzC,sCAAsC;gBACtC,OAAO,CAAC,IAAI,CACV,kHAAkH,EAClH,OAAO,CACR,CAAC;YACJ,CAAC;YACA,MAAkD,CAAC,IAAI,CAAC,GAAG,KAAK,CAAC;YAClE,OAAO,IAAI,CAAC;QACd,CAAC;KACF,CAAC,CAAC;AACL,CAAC;AAED,0FAA0F;AAC1F,yFAAyF;AACzF,2EAA2E;AAC3E,EAAE;AACF,yFAAyF;AACzF,8EAA8E;AAC9E,wBAAwB;AACxB,MAAM,UAAU,yBAAyB;IACvC,IAAI,OAAO,KAAK,KAAK,WAAW;QAAE,OAAO;IACzC,cAAc,GAAG,IAAI,CAAC;AACxB,CAAC;AAED,IAAI,cAAc,GAAG,KAAK,CAAC"}
1
+ {"version":3,"file":"guards.js","sourceRoot":"","sources":["../src/guards.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,gBAAgB,EAAE,MAAM,0BAA0B,CAAC;AAE5D,yFAAyF;AACzF,MAAM,UAAU,6BAA6B;IAC3C,OAAO,cAAc,CAAC;AACxB,CAAC;AAED,uFAAuF;AACvF,uFAAuF;AACvF,yCAAyC;AACzC,MAAM,UAAU,mBAAmB,CAAsB,MAAqB;IAC5E,IAAI,CAAC,cAAc,IAAI,OAAO,KAAK,KAAK,WAAW;QAAE,OAAO,MAAM,CAAC;IACnE,OAAO,IAAI,KAAK,CAAC,MAAM,EAAE;QACvB,GAAG,CAAC,MAAM,EAAE,IAAI,EAAE,KAAK;YACrB,IAAI,IAAI,KAAK,gBAAgB,IAAI,cAAc,EAAE,CAAC;gBAChD,wFAAwF;gBACxF,mGAAmG;gBACnG,mGAAmG;gBACnG,wCAAwC;gBACxC,WAAW,EAAE,CAAC,cAAc,CAAC,CAAC;YAChC,CAAC;YACA,MAAkD,CAAC,IAAI,CAAC,GAAG,KAAK,CAAC;YAClE,OAAO,IAAI,CAAC;QACd,CAAC;KACF,CAAC,CAAC;AACL,CAAC;AAED,0FAA0F;AAC1F,2FAA2F;AAC3F,eAAe;AACf,MAAM,UAAU,0BAA0B,CAAC,OAAsB;IAC/D,IAAI,CAAC,cAAc,IAAI,OAAO,KAAK,KAAK,WAAW;QAAE,OAAO,OAAO,CAAC;IACpE,OAAO,IAAI,KAAK,CAAC,OAAO,EAAE;QACxB,GAAG,CAAC,MAAM,EAAE,IAAI,EAAE,KAAK;YACrB,IAAI,IAAI,KAAK,SAAS,IAAI,cAAc,EAAE,CAAC;gBACzC,WAAW,EAAE,CAAC,cAAc,CAAC,CAAC;YAChC,CAAC;YACA,MAAkD,CAAC,IAAI,CAAC,GAAG,KAAK,CAAC;YAClE,OAAO,IAAI,CAAC;QACd,CAAC;KACF,CAAC,CAAC;AACL,CAAC;AAED,wGAAwG;AACxG,4FAA4F;AAC5F,MAAM,UAAU,yBAAyB,CAAC,OAAgB;IACxD,IAAI,OAAO,IAAI,OAAO,KAAK,KAAK,WAAW;QAAE,OAAO;IACpD,cAAc,GAAG,OAAO,CAAC;AAC3B,CAAC;AAED,wGAAwG;AACxG,0GAA0G;AAC1G,MAAM,UAAU,0BAA0B,CAAC,KAAqC;IAC9E,WAAW,GAAG,KAAK,CAAC;AACtB,CAAC;AAED,IAAI,cAAc,GAAG,KAAK,CAAC;AAC3B,IAAI,WAAW,GAAmC,IAAI,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,8 @@
1
1
  {
2
2
  "name": "@flighthq/entity",
3
- "version": "0.3.0-next.906.07cea63",
3
+ "version": "0.3.0",
4
+ "author": "Joshua Granick and other contributors",
5
+ "license": "MIT",
4
6
  "repository": {
5
7
  "type": "git",
6
8
  "url": "https://github.com/flighthq/flight.git",
@@ -36,7 +38,8 @@
36
38
  "clean:dist": "tsx ../../scripts/clean-package-dist.ts"
37
39
  },
38
40
  "dependencies": {
39
- "@flighthq/types": "0.3.0-next.906.07cea63"
41
+ "@flighthq/log": "0.3.0",
42
+ "@flighthq/types": "0.3.0"
40
43
  },
41
44
  "devDependencies": {
42
45
  "typescript": "^5.3.0"
@@ -0,0 +1,73 @@
1
+ import { addLogSink, createMemoryLogSink, getMemoryLogSinkEntries, removeLogSink } from '@flighthq/log/contract';
2
+ import type { LogEntry } from '@flighthq/types/contract';
3
+ import { EntityRuntimeKey } from '@flighthq/types/contract';
4
+
5
+ import { disableEntityRuntimeGuards, enableEntityRuntimeGuards } from './enableEntityRuntimeGuards';
6
+ import { createEntity } from './entity';
7
+ import { areEntityRuntimeGuardsEnabled, createGuardedEntity, createGuardedEntityRuntime } from './guards';
8
+ import { createEntityRuntime } from './runtime';
9
+
10
+ function captureLog(run: () => void): readonly LogEntry[] {
11
+ const sink = createMemoryLogSink(8);
12
+ addLogSink(sink.sink);
13
+ try {
14
+ run();
15
+ return getMemoryLogSinkEntries(sink);
16
+ } finally {
17
+ removeLogSink(sink.sink);
18
+ }
19
+ }
20
+
21
+ function messageOf(entry: Readonly<LogEntry>): string {
22
+ const data = entry.data;
23
+ return typeof data === 'string' ? data : String(data.message);
24
+ }
25
+
26
+ describe('disableEntityRuntimeGuards', () => {
27
+ it('uninstalls both the proxies and the reporter', () => {
28
+ const entries = captureLog(() => {
29
+ enableEntityRuntimeGuards();
30
+ disableEntityRuntimeGuards();
31
+ createGuardedEntity(createEntity())[EntityRuntimeKey] = undefined;
32
+ });
33
+ expect(entries.length).toBe(0);
34
+ expect(areEntityRuntimeGuardsEnabled()).toBe(false);
35
+ });
36
+ });
37
+
38
+ describe('enableEntityRuntimeGuards', () => {
39
+ it('WARNS through the log sink on a direct runtime-slot write', () => {
40
+ const entries = captureLog(() => {
41
+ enableEntityRuntimeGuards();
42
+ try {
43
+ createGuardedEntity(createEntity())[EntityRuntimeKey] = undefined;
44
+ } finally {
45
+ disableEntityRuntimeGuards();
46
+ }
47
+ });
48
+ expect(entries.length).toBe(1);
49
+ expect(messageOf(entries[0])).toContain('ensureEntityRuntime');
50
+ });
51
+
52
+ it('WARNS separately for a direct binding-slot write', () => {
53
+ const entries = captureLog(() => {
54
+ enableEntityRuntimeGuards();
55
+ try {
56
+ createGuardedEntityRuntime(createEntityRuntime()).binding = null;
57
+ } finally {
58
+ disableEntityRuntimeGuards();
59
+ }
60
+ });
61
+ expect(entries.length).toBe(1);
62
+ expect(messageOf(entries[0])).toContain('attachEntityBinding');
63
+ });
64
+
65
+ it('stays SILENT without the guard — the production default', () => {
66
+ const entries = captureLog(() => {
67
+ // Unguarded entities are returned as-is, so there is nothing to intercept.
68
+ createGuardedEntity(createEntity())[EntityRuntimeKey] = undefined;
69
+ });
70
+ expect(entries.length).toBe(0);
71
+ expect(areEntityRuntimeGuardsEnabled()).toBe(false);
72
+ });
73
+ });
@@ -1,16 +1,18 @@
1
1
  import { EntityRuntimeKey } from '@flighthq/types/contract';
2
+ import type { EntityRuntimeWriteSlot } from '@flighthq/types/contract';
2
3
 
3
4
  import { createEntity } from './entity';
4
5
  import {
5
6
  areEntityRuntimeGuardsEnabled,
6
7
  createGuardedEntity,
7
8
  createGuardedEntityRuntime,
8
- enableEntityRuntimeGuards,
9
+ setEntityRuntimeGuardMode,
10
+ setEntityRuntimeWriteGuard,
9
11
  } from './guards';
10
12
  import { createEntityRuntime } from './runtime';
11
13
 
12
14
  describe('areEntityRuntimeGuardsEnabled', () => {
13
- it('returns false before enableEntityRuntimeGuards is called', () => {
15
+ it('returns false before guard mode is switched on', () => {
14
16
  // Vitest isolates modules per file, so this starts false in a fresh module import.
15
17
  expect(areEntityRuntimeGuardsEnabled()).toBe(false);
16
18
  });
@@ -19,46 +21,61 @@ describe('areEntityRuntimeGuardsEnabled', () => {
19
21
  describe('createGuardedEntity', () => {
20
22
  it('returns the entity unchanged when guards are not enabled', () => {
21
23
  const entity = createEntity({ x: 1 });
22
- const guarded = createGuardedEntity(entity);
23
- expect(guarded).toBe(entity);
24
+ expect(createGuardedEntity(entity)).toBe(entity);
24
25
  });
25
26
  });
26
27
 
27
28
  describe('createGuardedEntityRuntime', () => {
28
29
  it('returns the runtime unchanged when guards are not enabled', () => {
29
30
  const runtime = createEntityRuntime();
30
- const guarded = createGuardedEntityRuntime(runtime);
31
- expect(guarded).toBe(runtime);
31
+ expect(createGuardedEntityRuntime(runtime)).toBe(runtime);
32
32
  });
33
33
  });
34
34
 
35
- describe('enableEntityRuntimeGuards', () => {
36
- it('enables guards', () => {
37
- enableEntityRuntimeGuards();
38
- expect(areEntityRuntimeGuardsEnabled()).toBe(true);
39
- });
40
-
41
- it('createGuardedEntity returns a proxy when guards are enabled', () => {
42
- enableEntityRuntimeGuards();
43
- const entity = createEntity({ x: 1 });
44
- const guarded = createGuardedEntity(entity);
45
- expect(guarded.x).toBe(1);
35
+ describe('setEntityRuntimeGuardMode', () => {
36
+ it('switches the proxies on and back off', () => {
37
+ setEntityRuntimeGuardMode(true);
38
+ try {
39
+ expect(areEntityRuntimeGuardsEnabled()).toBe(true);
40
+ const entity = createEntity({ x: 1 });
41
+ expect(createGuardedEntity(entity).x).toBe(1);
42
+ expect(createGuardedEntityRuntime(createEntityRuntime()).binding).toBeNull();
43
+ } finally {
44
+ setEntityRuntimeGuardMode(false);
45
+ }
46
+ expect(areEntityRuntimeGuardsEnabled()).toBe(false);
46
47
  });
48
+ });
47
49
 
48
- it('createGuardedEntityRuntime returns a proxy when guards are enabled', () => {
49
- enableEntityRuntimeGuards();
50
- const runtime = createEntityRuntime();
51
- const guarded = createGuardedEntityRuntime(runtime);
52
- expect(guarded.binding).toBeNull();
50
+ describe('setEntityRuntimeWriteGuard', () => {
51
+ it('reports which slot a direct write landed on, and still ALLOWS the write', () => {
52
+ const slots: EntityRuntimeWriteSlot[] = [];
53
+ setEntityRuntimeGuardMode(true);
54
+ setEntityRuntimeWriteGuard((slot) => slots.push(slot));
55
+ try {
56
+ const guardedEntity = createGuardedEntity(createEntity());
57
+ guardedEntity[EntityRuntimeKey] = undefined;
58
+ const guardedRuntime = createGuardedEntityRuntime(createEntityRuntime());
59
+ guardedRuntime.binding = null;
60
+ expect(slots).toEqual(['runtime-slot', 'binding-slot']);
61
+ // The guard observes; it does not block. The write must land.
62
+ expect(EntityRuntimeKey in guardedEntity).toBe(true);
63
+ } finally {
64
+ setEntityRuntimeWriteGuard(null);
65
+ setEntityRuntimeGuardMode(false);
66
+ }
53
67
  });
54
68
 
55
- it('guarded entity warns on direct EntityRuntimeKey write', () => {
56
- enableEntityRuntimeGuards();
57
- const entity = createEntity();
58
- const guarded = createGuardedEntity(entity);
59
- const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => undefined);
60
- guarded[EntityRuntimeKey] = undefined;
61
- expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining('[entity]'), expect.anything());
62
- warnSpy.mockRestore();
69
+ it('is silent once uninstalled', () => {
70
+ const slots: EntityRuntimeWriteSlot[] = [];
71
+ setEntityRuntimeGuardMode(true);
72
+ setEntityRuntimeWriteGuard((slot) => slots.push(slot));
73
+ setEntityRuntimeWriteGuard(null);
74
+ try {
75
+ createGuardedEntity(createEntity())[EntityRuntimeKey] = undefined;
76
+ expect(slots).toEqual([]);
77
+ } finally {
78
+ setEntityRuntimeGuardMode(false);
79
+ }
63
80
  });
64
81
  });