{"version":3,"file":"storm.test.d.ts","sourceRoot":"","sources":["../../../src/core/storm/storm.test.ts"],"names":[],"mappings":"","sourcesContent":["import type { AgentToolResult } from \"@apholdings/jensen-agent-core\";\nimport { describe, expect, it } from \"vitest\";\nimport { StormBreaker, TOOL_CALL_STORM_BLOCKED, TOOL_STRATEGY_PIVOT_REQUIRED } from \"./index.js\";\n\nconst result = (text: string): AgentToolResult<any> => ({\n\tcontent: [{ type: \"text\", text }],\n\tdetails: {},\n});\n\ndescribe(\"storm breaker\", () => {\n\tit(\"classifies a first call as fresh\", () => {\n\t\tconst breaker = new StormBreaker();\n\t\tconst d = breaker.classify({ toolName: \"read\", canonicalArgsHash: \"h\", progressSignals: [], readOnly: true });\n\t\texpect(d.stage).toBe(\"fresh\");\n\t\texpect(d.execute).toBe(true);\n\t});\n\n\tit(\"annotates repeated identical read calls with no state change\", () => {\n\t\tconst breaker = new StormBreaker();\n\t\tbreaker.classify({ toolName: \"read\", canonicalArgsHash: \"h\", progressSignals: [], readOnly: true });\n\t\tconst d = breaker.classify({ toolName: \"read\", canonicalArgsHash: \"h\", progressSignals: [], readOnly: true });\n\t\texpect(d.stage).toBe(\"duplicate_annotate\");\n\t});\n\n\tit(\"escalates to reflect then block then pivot\", () => {\n\t\tconst breaker = new StormBreaker();\n\t\tlet last: ReturnType<StormBreaker[\"classify\"]> | null = null;\n\t\tfor (let i = 0; i < 7; i++) {\n\t\t\tlast = breaker.classify({ toolName: \"read\", canonicalArgsHash: \"h\", progressSignals: [], readOnly: true });\n\t\t}\n\t\texpect(last!.stage).toBe(\"strategy_pivot_required\");\n\t\texpect(last!.execute).toBe(false);\n\n\t\tconst breaker2 = new StormBreaker();\n\t\tlet blocked: ReturnType<StormBreaker[\"classify\"]> | null = null;\n\t\tfor (let i = 0; i < 4; i++) {\n\t\t\tblocked = breaker2.classify({ toolName: \"read\", canonicalArgsHash: \"h\", progressSignals: [], readOnly: true });\n\t\t}\n\t\texpect(blocked!.stage).toBe(\"storm_blocked\");\n\t\tconst b = blocked!;\n\t\tif (b.stage === \"storm_blocked\") {\n\t\t\texpect(b.errorCode).toBe(TOOL_CALL_STORM_BLOCKED);\n\t\t\texpect(b.execute).toBe(false);\n\t\t}\n\t});\n\n\tit(\"resets duplicate detection when authoritative state changes\", () => {\n\t\tconst breaker = new StormBreaker();\n\t\tbreaker.classify({ toolName: \"read\", canonicalArgsHash: \"h\", progressSignals: [], readOnly: true });\n\t\tbreaker.classify({ toolName: \"read\", canonicalArgsHash: \"h\", progressSignals: [], readOnly: true });\n\t\t// New file content hash => authoritative progress.\n\t\tconst d = breaker.classify({\n\t\t\ttoolName: \"read\",\n\t\t\tcanonicalArgsHash: \"h\",\n\t\t\tprogressSignals: [{ kind: \"file_content_hash\", path: \"a.ts\", hash: \"abc\" }],\n\t\t\treadOnly: true,\n\t\t});\n\t\texpect(d.stage).toBe(\"fresh\");\n\t});\n\n\tit(\"reuses a prior authoritative result only for read-only calls\", () => {\n\t\tconst breaker = new StormBreaker();\n\t\tconst prior = result(\"file contents\");\n\t\tconst opts = {\n\t\t\treadOnly: true,\n\t\t\tprogressSignals: [] as unknown[] as never,\n\t\t\tcurrentValidityFingerprint: \"v1\",\n\t\t\tpriorResult: { result: prior, validityFingerprint: \"v1\", at: 0 },\n\t\t};\n\t\tbreaker.recordResult({ toolName: \"read\", canonicalArgsHash: \"h\", result: prior, validityFingerprint: \"v1\" });\n\t\t// First classify after recording the result is the 1st duplicate → annotate with cached result.\n\t\tconst d = breaker.classify({ toolName: \"read\", canonicalArgsHash: \"h\", ...opts } as never);\n\t\texpect(d.stage).toBe(\"duplicate_annotate\");\n\t\tif (d.stage === \"duplicate_annotate\") {\n\t\t\texpect(d.cachedResult?.content[0]).toEqual({ type: \"text\", text: \"file contents\" });\n\t\t}\n\t});\n\n\tit(\"never treats mutating calls as reusable duplicates\", () => {\n\t\tconst breaker = new StormBreaker();\n\t\t// A mutating call repeated is still blocked, never silently replayed.\n\t\tbreaker.classify({ toolName: \"write\", canonicalArgsHash: \"h\", progressSignals: [], readOnly: false });\n\t\tconst d = breaker.classify({ toolName: \"write\", canonicalArgsHash: \"h\", progressSignals: [], readOnly: false });\n\t\texpect(d.stage).toBe(\"duplicate_annotate\");\n\t\tif (d.stage === \"duplicate_annotate\") {\n\t\t\texpect(d.cachedResult).toBeUndefined();\n\t\t}\n\t});\n\n\tit(\"blocks a storm loop and reports typed pivot\", () => {\n\t\tconst breaker = new StormBreaker();\n\t\tlet pivoted = false;\n\t\tfor (let i = 0; i < 7; i++) {\n\t\t\tconst d = breaker.classify({ toolName: \"grep\", canonicalArgsHash: \"q\", progressSignals: [], readOnly: true });\n\t\t\tif (d.stage === \"strategy_pivot_required\") {\n\t\t\t\texpect(d.errorCode).toBe(TOOL_STRATEGY_PIVOT_REQUIRED);\n\t\t\t\tpivoted = true;\n\t\t\t}\n\t\t}\n\t\texpect(pivoted).toBe(true);\n\t});\n});\n"]}