fix(rete): inject clock into EventLog; use tsc for DTS; fix cycle.test.ts private access; add Playwright worker limit
This commit is contained in:
parent
103f2bd0a6
commit
ca6a3df500
5 changed files with 71 additions and 63 deletions
|
|
@ -13,7 +13,7 @@
|
|||
}
|
||||
},
|
||||
"scripts": {
|
||||
"build": "tsup src/index.ts --format esm,cjs --dts --clean",
|
||||
"build": "tsup src/index.ts --format esm,cjs --clean && tsc -p tsconfig.build.json --emitDeclarationOnly --noEmitOnError",
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
|
|
|
|||
|
|
@ -33,87 +33,77 @@ describe("RecursionLimitExceededError", () => {
|
|||
});
|
||||
|
||||
describe("Session.fireRules() recursion limit", () => {
|
||||
it("throws RecursionLimitExceededError when recursion depth exceeds limit", () => {
|
||||
const session = new Session({ autoFire: false, recursionLimit: 3 });
|
||||
expect(() => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
(session as any)._fireDepth = 4;
|
||||
it("throws RecursionLimitExceededError when recursion depth exceeds limit", () => {
|
||||
const session = new Session({ autoFire: false, recursionLimit: 3 });
|
||||
expect(() => {
|
||||
session._fireDepth = 4;
|
||||
session.fireRules({ recursionLimit: 3 });
|
||||
}).toThrow(RecursionLimitExceededError);
|
||||
});
|
||||
|
||||
it("throws when depth equals limit (>=)", () => {
|
||||
const session = new Session({ autoFire: false, recursionLimit: 3 });
|
||||
expect(() => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
(session as any)._fireDepth = 3;
|
||||
it("throws when depth equals limit (>=)", () => {
|
||||
const session = new Session({ autoFire: false, recursionLimit: 3 });
|
||||
expect(() => {
|
||||
session._fireDepth = 3;
|
||||
session.fireRules({ recursionLimit: 3 });
|
||||
}).toThrow(RecursionLimitExceededError);
|
||||
});
|
||||
|
||||
it("does not throw when depth is below limit", () => {
|
||||
const session = new Session({ autoFire: false, recursionLimit: 64 });
|
||||
expect(() => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
(session as any)._fireDepth = 0;
|
||||
it("does not throw when depth is below limit", () => {
|
||||
const session = new Session({ autoFire: false, recursionLimit: 64 });
|
||||
expect(() => {
|
||||
session._fireDepth = 0;
|
||||
session.fireRules({ recursionLimit: 64 });
|
||||
}).not.toThrow();
|
||||
});
|
||||
|
||||
it("uses session default recursionLimit when opts omitted", () => {
|
||||
const session = new Session({ autoFire: false, recursionLimit: 3 });
|
||||
expect(() => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
(session as any)._fireDepth = 5;
|
||||
it("uses session default recursionLimit when opts omitted", () => {
|
||||
const session = new Session({ autoFire: false, recursionLimit: 3 });
|
||||
expect(() => {
|
||||
session._fireDepth = 5;
|
||||
session.fireRules();
|
||||
}).toThrow(RecursionLimitExceededError);
|
||||
});
|
||||
|
||||
it("resets recursion depth to 0 between independent fireRules() calls", () => {
|
||||
const session = new Session({ autoFire: false, recursionLimit: 3 });
|
||||
expect(() => session.fireRules()).not.toThrow();
|
||||
expect(() => session.fireRules()).not.toThrow();
|
||||
expect(() => session.fireRules()).not.toThrow();
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
expect((session as any)._fireDepth).toBe(0);
|
||||
it("resets recursion depth to 0 between independent fireRules() calls", () => {
|
||||
const session = new Session({ autoFire: false, recursionLimit: 3 });
|
||||
expect(() => session.fireRules()).not.toThrow();
|
||||
expect(() => session.fireRules()).not.toThrow();
|
||||
expect(() => session.fireRules()).not.toThrow();
|
||||
expect(session._fireDepth).toBe(0);
|
||||
});
|
||||
|
||||
it("decrements depth even if fireRules body were to throw (finally)", () => {
|
||||
const session = new Session({ autoFire: false, recursionLimit: 3 });
|
||||
// Force recursion guard to throw; internal counter must not be left elevated
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
(session as any)._fireDepth = 5;
|
||||
expect(() => session.fireRules({ recursionLimit: 3 })).toThrow(
|
||||
RecursionLimitExceededError,
|
||||
);
|
||||
// After throw, depth should still be 5 (guard throws before increment)
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
expect((session as any)._fireDepth).toBe(5);
|
||||
it("decrements depth even if fireRules body were to throw (finally)", () => {
|
||||
const session = new Session({ autoFire: false, recursionLimit: 3 });
|
||||
// Force recursion guard to throw; internal counter must not be left elevated
|
||||
session._fireDepth = 5;
|
||||
expect(() => session.fireRules({ recursionLimit: 3 })).toThrow(
|
||||
RecursionLimitExceededError,
|
||||
);
|
||||
// After throw, depth should still be 5 (guard throws before increment)
|
||||
expect(session._fireDepth).toBe(5);
|
||||
});
|
||||
|
||||
it("recursionLimit: 0 means no limit — does not throw", () => {
|
||||
const session = new Session({ autoFire: false, recursionLimit: 0 });
|
||||
expect(() => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
(session as any)._fireDepth = 1000;
|
||||
it("recursionLimit: 0 means no limit — does not throw", () => {
|
||||
const session = new Session({ autoFire: false, recursionLimit: 0 });
|
||||
expect(() => {
|
||||
session._fireDepth = 1000;
|
||||
session.fireRules({ recursionLimit: 0 });
|
||||
}).not.toThrow();
|
||||
});
|
||||
|
||||
it("recursionLimit: 0 at session level also means no limit", () => {
|
||||
const session = new Session({ autoFire: false, recursionLimit: 0 });
|
||||
expect(() => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
(session as any)._fireDepth = 1000;
|
||||
it("recursionLimit: 0 at session level also means no limit", () => {
|
||||
const session = new Session({ autoFire: false, recursionLimit: 0 });
|
||||
expect(() => {
|
||||
session._fireDepth = 1000;
|
||||
session.fireRules();
|
||||
}).not.toThrow();
|
||||
});
|
||||
|
||||
it("thrown error carries the actual depth value", () => {
|
||||
const session = new Session({ autoFire: false, recursionLimit: 3 });
|
||||
try {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
(session as any)._fireDepth = 7;
|
||||
it("thrown error carries the actual depth value", () => {
|
||||
const session = new Session({ autoFire: false, recursionLimit: 3 });
|
||||
try {
|
||||
session._fireDepth = 7;
|
||||
session.fireRules({ recursionLimit: 3 });
|
||||
expect.fail("should have thrown");
|
||||
} catch (err) {
|
||||
|
|
@ -122,12 +112,11 @@ describe("Session.fireRules() recursion limit", () => {
|
|||
}
|
||||
});
|
||||
|
||||
it("opts.recursionLimit overrides the session default", () => {
|
||||
const session = new Session({ autoFire: false, recursionLimit: 64 });
|
||||
// Session default is 64 — would not throw. But opts override to 2.
|
||||
expect(() => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
(session as any)._fireDepth = 2;
|
||||
it("opts.recursionLimit overrides the session default", () => {
|
||||
const session = new Session({ autoFire: false, recursionLimit: 64 });
|
||||
// Session default is 64 — would not throw. But opts override to 2.
|
||||
expect(() => {
|
||||
session._fireDepth = 2;
|
||||
session.fireRules({ recursionLimit: 2 });
|
||||
}).toThrow(RecursionLimitExceededError);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -48,6 +48,16 @@ export interface LogEvent {
|
|||
export class EventLog {
|
||||
readonly #events: LogEvent[] = [];
|
||||
#seq = 0;
|
||||
readonly #clock: () => number;
|
||||
|
||||
/**
|
||||
* @param clock — Optional wall-clock function. Defaults to `Date.now`.
|
||||
* Inject a deterministic clock (e.g. `() => 0`) in tests and replay
|
||||
* contexts where timestamp values must be stable across runs.
|
||||
*/
|
||||
constructor(clock: () => number = Date.now) {
|
||||
this.#clock = clock;
|
||||
}
|
||||
|
||||
/** Number of events logged. */
|
||||
get length(): number {
|
||||
|
|
@ -67,7 +77,7 @@ export class EventLog {
|
|||
appendInsert(id: EntityId, attr: AttrKey, value: FactValue): void {
|
||||
this.#events.push({
|
||||
seq: ++this.#seq,
|
||||
ts: Date.now(),
|
||||
ts: this.#clock(),
|
||||
kind: "insert",
|
||||
id,
|
||||
attr,
|
||||
|
|
@ -82,7 +92,7 @@ export class EventLog {
|
|||
appendRetract(id: EntityId, attr: AttrKey, value: FactValue): void {
|
||||
this.#events.push({
|
||||
seq: ++this.#seq,
|
||||
ts: Date.now(),
|
||||
ts: this.#clock(),
|
||||
kind: "retract",
|
||||
id,
|
||||
attr,
|
||||
|
|
@ -94,7 +104,7 @@ export class EventLog {
|
|||
appendFire(ruleName: string): void {
|
||||
this.#events.push({
|
||||
seq: ++this.#seq,
|
||||
ts: Date.now(),
|
||||
ts: this.#clock(),
|
||||
kind: "fire",
|
||||
ruleName,
|
||||
});
|
||||
|
|
|
|||
8
packages/rete/tsconfig.build.json
Normal file
8
packages/rete/tsconfig.build.json
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"composite": false,
|
||||
"incremental": false,
|
||||
"declarationMap": false
|
||||
}
|
||||
}
|
||||
|
|
@ -15,4 +15,5 @@ export default defineConfig({
|
|||
timeout: 30000,
|
||||
},
|
||||
reporter: [["html", { open: "never" }], ["list"]],
|
||||
workers: 1, // prevent parallel workers from sharing the single dev server
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue