pi-token-burden 0.1.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.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Will Hampson
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,79 @@
1
+ # pi-token-burden
2
+
3
+ See where your system prompt tokens go.
4
+
5
+ A [pi](https://github.com/mariozechner/pi) extension that parses the assembled system prompt and shows a token-budget breakdown by section. Run `/token-burden` to see how much of your context window is consumed by the base prompt, AGENTS.md files, skills, SYSTEM.md overrides, and metadata.
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ pi install git:github.com/Whamp/pi-token-burden
11
+ ```
12
+
13
+ Or try it for a single session:
14
+
15
+ ```bash
16
+ pi -e git:github.com/Whamp/pi-token-burden
17
+ ```
18
+
19
+ ## Usage
20
+
21
+ Once installed, type `/token-burden` in any pi session. A TUI panel shows:
22
+
23
+ ```
24
+ Token Burden
25
+ System Prompt: 12,450 tokens (49,798 chars) — 6.2% of 200,000 context window
26
+
27
+ Base prompt 1,250 tokens 10.0%
28
+ SYSTEM.md / APPEND_SYSTEM.md 340 tokens 2.7%
29
+ AGENTS.md files 2,860 tokens 23.0%
30
+ /home/user/.pi/agent/AGENTS.md 1,100 tokens 8.8%
31
+ /home/user/project/AGENTS.md 1,760 tokens 14.1%
32
+ Skills (42) 7,800 tokens 62.6%
33
+ brainstorming 180 tokens 1.4%
34
+ tdd 165 tokens 1.3%
35
+ ...
36
+ Metadata (date/time, cwd) 200 tokens 1.6%
37
+
38
+ Press Enter or Esc to close
39
+ ```
40
+
41
+ Sections exceeding 10% of the system prompt are highlighted.
42
+
43
+ ## Sections
44
+
45
+ | Section | What it measures |
46
+ | -------------------------------- | ---------------------------------------------------------------- |
47
+ | **Base prompt** | pi's built-in instructions, tool descriptions, guidelines |
48
+ | **SYSTEM.md / APPEND_SYSTEM.md** | Your custom system prompt overrides |
49
+ | **AGENTS.md files** | Each AGENTS.md file, listed individually |
50
+ | **Skills** | The `<available_skills>` block, with per-skill breakdown |
51
+ | **Metadata** | The `Current date and time` / `Current working directory` footer |
52
+
53
+ ## Token estimation
54
+
55
+ Tokens are estimated as `ceil(chars / 4)` — the same heuristic pi uses internally. This is a rough approximation, not a tokenizer count.
56
+
57
+ ## Requirements
58
+
59
+ - [pi](https://github.com/mariozechner/pi) v0.55.1 or later
60
+
61
+ ## Development
62
+
63
+ ```bash
64
+ git clone https://github.com/Whamp/pi-token-burden.git
65
+ cd pi-token-burden
66
+ pnpm install
67
+ pnpm run test # 15 tests
68
+ pnpm run check # lint, typecheck, format, dead code, duplicates, secrets, tests
69
+ ```
70
+
71
+ Test locally without installing:
72
+
73
+ ```bash
74
+ pi -e ./src/index.ts
75
+ ```
76
+
77
+ ## License
78
+
79
+ [MIT](LICENSE)
package/package.json ADDED
@@ -0,0 +1,58 @@
1
+ {
2
+ "name": "pi-token-burden",
3
+ "version": "0.1.0",
4
+ "description": "Pi extension that shows a token-budget breakdown of the assembled system prompt",
5
+ "keywords": [
6
+ "pi-extension",
7
+ "pi-package",
8
+ "token-budget"
9
+ ],
10
+ "license": "MIT",
11
+ "files": [
12
+ "src/",
13
+ "LICENSE",
14
+ "README.md"
15
+ ],
16
+ "type": "module",
17
+ "scripts": {
18
+ "lint": "oxlint -c .oxlintrc.json",
19
+ "lint:fix": "oxlint -c .oxlintrc.json --fix",
20
+ "format": "oxfmt --config .oxfmtrc.jsonc",
21
+ "format:check": "oxfmt --config .oxfmtrc.jsonc --check",
22
+ "typecheck": "tsc --noEmit",
23
+ "test": "vitest run",
24
+ "deadcode": "knip",
25
+ "duplicates": "jscpd src/",
26
+ "secrets": "gitleaks detect --source . --no-git",
27
+ "check": "bash scripts/check.sh",
28
+ "fix": "bash scripts/fix.sh"
29
+ },
30
+ "devDependencies": {
31
+ "@types/node": "^25.3.2",
32
+ "gitleaks": "^1.0.0",
33
+ "husky": "^9.1.7",
34
+ "jscpd": "^4.0.8",
35
+ "knip": "^5.85.0",
36
+ "lint-staged": "^16.2.7",
37
+ "oxfmt": "^0.35.0",
38
+ "oxlint": "^1.50.0",
39
+ "typescript": "^5.9.3",
40
+ "ultracite": "^7.2.4",
41
+ "vitest": "^4.0.18"
42
+ },
43
+ "peerDependencies": {
44
+ "@mariozechner/pi-coding-agent": "*",
45
+ "@mariozechner/pi-tui": "*",
46
+ "@sinclair/typebox": "*"
47
+ },
48
+ "pnpm": {
49
+ "onlyBuiltDependencies": [
50
+ "esbuild"
51
+ ]
52
+ },
53
+ "pi": {
54
+ "extensions": [
55
+ "./src/index.ts"
56
+ ]
57
+ }
58
+ }
@@ -0,0 +1,6 @@
1
+ describe("extension", () => {
2
+ it("exports a default function", async () => {
3
+ const mod = await import("./index.js");
4
+ expectTypeOf(mod.default).toBeFunction();
5
+ });
6
+ });
package/src/index.ts ADDED
@@ -0,0 +1,25 @@
1
+ import type { ExtensionFactory } from "@mariozechner/pi-coding-agent";
2
+
3
+ import { parseSystemPrompt } from "./parser.js";
4
+ import { showReport } from "./report-view.js";
5
+
6
+ const extension: ExtensionFactory = (pi) => {
7
+ pi.registerCommand("token-burden", {
8
+ description: "Show token budget breakdown of the system prompt",
9
+ handler: async (_args, ctx) => {
10
+ const prompt = ctx.getSystemPrompt();
11
+ const parsed = parseSystemPrompt(prompt);
12
+
13
+ const usage = ctx.getContextUsage();
14
+ const contextWindow = usage?.contextWindow ?? ctx.model?.contextWindow;
15
+
16
+ if (!ctx.hasUI) {
17
+ return;
18
+ }
19
+
20
+ await showReport(parsed, contextWindow, ctx);
21
+ },
22
+ });
23
+ };
24
+
25
+ export default extension;
@@ -0,0 +1,156 @@
1
+ import { estimateTokens, parseSystemPrompt } from "./parser.js";
2
+ import type { ParsedPrompt } from "./parser.js";
3
+
4
+ describe("estimateTokens()", () => {
5
+ it("returns ceil(chars / 4)", () => {
6
+ expect(estimateTokens("")).toBe(0);
7
+ expect(estimateTokens("abcd")).toBe(1);
8
+ expect(estimateTokens("abcde")).toBe(2);
9
+ expect(estimateTokens("a")).toBe(1);
10
+ });
11
+ });
12
+
13
+ describe("parseSystemPrompt()", () => {
14
+ const basePrompt = [
15
+ "You are an expert coding assistant operating inside pi.",
16
+ "",
17
+ "Available tools:",
18
+ "- read: Read file contents",
19
+ "- bash: Execute bash commands",
20
+ "",
21
+ "Guidelines:",
22
+ "- Be concise",
23
+ "",
24
+ "Pi documentation (read only when the user asks about pi itself):",
25
+ "- Main documentation: /path/to/README.md",
26
+ "- Always read pi .md files completely and follow links to related docs",
27
+ ].join("\n");
28
+
29
+ const agentsBlock = [
30
+ "",
31
+ "",
32
+ "# Project Context",
33
+ "",
34
+ "Project-specific instructions and guidelines:",
35
+ "",
36
+ "## /home/user/.pi/agent/AGENTS.md",
37
+ "",
38
+ "# Global Agent Guidelines",
39
+ "",
40
+ "## Before Acting",
41
+ "- Read files before editing.",
42
+ "",
43
+ "## /home/user/project/AGENTS.md",
44
+ "",
45
+ "# Project Rules",
46
+ "",
47
+ "- Follow TDD.",
48
+ ].join("\n");
49
+
50
+ const skillsPreamble = [
51
+ "",
52
+ "",
53
+ "The following skills provide specialized instructions for specific tasks.",
54
+ "Use the read tool to load a skill's file when the task matches its description.",
55
+ "When a skill file references a relative path, resolve it against the skill directory.",
56
+ "",
57
+ ].join("\n");
58
+
59
+ const skillsBlock = [
60
+ "<available_skills>",
61
+ " <skill>",
62
+ " <name>brainstorming</name>",
63
+ " <description>Explore user intent before implementation.</description>",
64
+ " <location>/home/user/skills/brainstorming/SKILL.md</location>",
65
+ " </skill>",
66
+ " <skill>",
67
+ " <name>tdd</name>",
68
+ " <description>Test-driven development workflow.</description>",
69
+ " <location>/home/user/skills/tdd/SKILL.md</location>",
70
+ " </skill>",
71
+ "</available_skills>",
72
+ ].join("\n");
73
+
74
+ const metadata =
75
+ "\nCurrent date and time: Thursday, February 26, 2026\nCurrent working directory: /home/user/project";
76
+
77
+ it("parses a full system prompt into sections", () => {
78
+ const prompt =
79
+ basePrompt + agentsBlock + skillsPreamble + skillsBlock + metadata;
80
+ const result: ParsedPrompt = parseSystemPrompt(prompt);
81
+
82
+ expect(result.totalChars).toBe(prompt.length);
83
+ expect(result.totalTokens).toBe(Math.ceil(prompt.length / 4));
84
+
85
+ const labels = result.sections.map((s) => s.label);
86
+ expect(labels).toContain("Base prompt");
87
+ expect(labels).toContain("AGENTS.md files");
88
+ });
89
+
90
+ it("parses a full system prompt with correct section labels", () => {
91
+ const prompt =
92
+ basePrompt + agentsBlock + skillsPreamble + skillsBlock + metadata;
93
+ const result = parseSystemPrompt(prompt);
94
+
95
+ const labels = result.sections.map((s) => s.label);
96
+ expect(labels).toContain("Skills (2)");
97
+ expect(labels).toContain("Metadata (date/time, cwd)");
98
+ });
99
+
100
+ it("parses AGENTS.md files into children", () => {
101
+ const prompt = basePrompt + agentsBlock + metadata;
102
+ const result = parseSystemPrompt(prompt);
103
+
104
+ const agentsSection = result.sections.find((s) =>
105
+ s.label.includes("AGENTS.md")
106
+ );
107
+ expect(agentsSection?.children).toHaveLength(2);
108
+ expect(agentsSection?.children?.[0].label).toBe(
109
+ "/home/user/.pi/agent/AGENTS.md"
110
+ );
111
+ expect(agentsSection?.children?.[1].label).toBe(
112
+ "/home/user/project/AGENTS.md"
113
+ );
114
+ });
115
+
116
+ it("parses individual skills from XML", () => {
117
+ const prompt = basePrompt + skillsPreamble + skillsBlock + metadata;
118
+ const result = parseSystemPrompt(prompt);
119
+
120
+ expect(result.skills).toHaveLength(2);
121
+ expect(result.skills[0].name).toBe("brainstorming");
122
+ expect(result.skills[1].name).toBe("tdd");
123
+ expect(result.skills[0].chars).toBeGreaterThan(0);
124
+ });
125
+
126
+ it("includes skill children in skills section", () => {
127
+ const prompt = basePrompt + skillsPreamble + skillsBlock + metadata;
128
+ const result = parseSystemPrompt(prompt);
129
+
130
+ const skillsSection = result.sections.find((s) =>
131
+ s.label.startsWith("Skills")
132
+ );
133
+ expect(skillsSection?.children).toHaveLength(2);
134
+ });
135
+
136
+ it("handles a minimal prompt with no optional sections", () => {
137
+ const prompt = `You are a helpful assistant.${metadata}`;
138
+ const result = parseSystemPrompt(prompt);
139
+
140
+ expect(result.sections.length).toBeGreaterThanOrEqual(1);
141
+ expect(result.totalChars).toBe(prompt.length);
142
+ });
143
+
144
+ it("detects SYSTEM.md / APPEND_SYSTEM.md content between base and project context", () => {
145
+ const appendContent =
146
+ "\n\nCustom SYSTEM.md instructions here.\nMore custom content.";
147
+ const prompt = basePrompt + appendContent + agentsBlock + metadata;
148
+ const result = parseSystemPrompt(prompt);
149
+
150
+ const systemMdSection = result.sections.find((s) =>
151
+ s.label.includes("SYSTEM.md")
152
+ );
153
+ expect(systemMdSection).toBeDefined();
154
+ expect(systemMdSection?.chars).toBeGreaterThan(0);
155
+ });
156
+ });
package/src/parser.ts ADDED
@@ -0,0 +1,244 @@
1
+ /**
2
+ * Parse the assembled system prompt into measurable sections.
3
+ *
4
+ * The system prompt built by pi follows a predictable structure:
5
+ * 1. Base prompt (tools, guidelines, pi docs reference)
6
+ * 2. Optional SYSTEM.md / APPEND_SYSTEM.md content
7
+ * 3. Project Context (AGENTS.md files, each under `## <path>`)
8
+ * 4. Skills preamble + <available_skills> block
9
+ * 5. Date/time + cwd metadata
10
+ */
11
+
12
+ import type {
13
+ AgentsFileEntry,
14
+ ParsedPrompt,
15
+ PromptSection,
16
+ SkillEntry,
17
+ } from "./types.js";
18
+
19
+ export type { ParsedPrompt };
20
+
21
+ /** Token estimate using pi's built-in heuristic: ceil(chars / 4). */
22
+ export function estimateTokens(text: string): number {
23
+ return Math.ceil(text.length / 4);
24
+ }
25
+
26
+ // ---------------------------------------------------------------------------
27
+ // Internal helpers (defined before use to satisfy no-use-before-define)
28
+ // ---------------------------------------------------------------------------
29
+
30
+ function measure(label: string, text: string): PromptSection {
31
+ return { label, chars: text.length, tokens: estimateTokens(text) };
32
+ }
33
+
34
+ /** Return the smallest positive value, or -1 if none are positive. */
35
+ function firstPositive(...values: number[]): number {
36
+ let min = -1;
37
+ for (const v of values) {
38
+ if (v >= 0 && (min < 0 || v < min)) {
39
+ min = v;
40
+ }
41
+ }
42
+ return min;
43
+ }
44
+
45
+ /**
46
+ * Find where the base system prompt ends.
47
+ *
48
+ * The base prompt ends after the pi docs reference block. We look for
49
+ * "- Always read pi .md files" or "- When working on pi" as the terminal
50
+ * marker. Falls back to the first major section boundary.
51
+ */
52
+ function findBasePromptEnd(
53
+ prompt: string,
54
+ projectCtxIdx: number,
55
+ skillsPreambleIdx: number,
56
+ dateLineIdx: number
57
+ ): number {
58
+ const piDocsMarker = /^- (?:Always read pi|When working on pi).+$/gm;
59
+ let lastPiDocsEnd = -1;
60
+ for (const match of prompt.matchAll(piDocsMarker)) {
61
+ lastPiDocsEnd = match.index + match[0].length;
62
+ }
63
+
64
+ if (lastPiDocsEnd !== -1) {
65
+ return lastPiDocsEnd;
66
+ }
67
+
68
+ return firstPositive(projectCtxIdx, skillsPreambleIdx, dateLineIdx);
69
+ }
70
+
71
+ /** Parse `## /path/to/AGENTS.md` blocks inside the Project Context section. */
72
+ function parseAgentsFiles(contextBlock: string): AgentsFileEntry[] {
73
+ const files: AgentsFileEntry[] = [];
74
+ // Match `## ` headings that look like file paths (start with `/`).
75
+ const headingPattern = /^## (\/.+)$/gm;
76
+ const matches = [...contextBlock.matchAll(headingPattern)];
77
+
78
+ for (let i = 0; i < matches.length; i++) {
79
+ const [, path] = matches[i];
80
+ const blockStart = matches[i].index;
81
+ const blockEnd =
82
+ i + 1 < matches.length ? matches[i + 1].index : contextBlock.length;
83
+ const blockText = contextBlock.slice(blockStart, blockEnd);
84
+ files.push({
85
+ path,
86
+ chars: blockText.length,
87
+ tokens: estimateTokens(blockText),
88
+ });
89
+ }
90
+
91
+ return files;
92
+ }
93
+
94
+ /** Parse `<skill>` entries from the `<available_skills>` XML block. */
95
+ function parseSkillEntries(xmlBlock: string, out: SkillEntry[]): void {
96
+ const skillPattern = /<skill>([\s\S]*?)<\/skill>/g;
97
+ const namePattern = /<name>([\s\S]*?)<\/name>/;
98
+ const descPattern = /<description>([\s\S]*?)<\/description>/;
99
+ const locPattern = /<location>([\s\S]*?)<\/location>/;
100
+
101
+ for (const match of xmlBlock.matchAll(skillPattern)) {
102
+ const [fullEntry, inner] = match;
103
+ const name = inner.match(namePattern)?.[1]?.trim() ?? "unknown";
104
+ const description = inner.match(descPattern)?.[1]?.trim() ?? "";
105
+ const location = inner.match(locPattern)?.[1]?.trim() ?? "";
106
+
107
+ out.push({
108
+ name,
109
+ description,
110
+ location,
111
+ chars: fullEntry.length,
112
+ tokens: estimateTokens(fullEntry),
113
+ });
114
+ }
115
+ }
116
+
117
+ /** Compute the skills section end index, avoiding nested ternaries. */
118
+ function findSkillsSectionEnd(
119
+ availableSkillsEnd: number,
120
+ dateLineIdx: number,
121
+ promptLength: number
122
+ ): number {
123
+ if (availableSkillsEnd !== -1) {
124
+ return availableSkillsEnd + "</available_skills>".length;
125
+ }
126
+ if (dateLineIdx !== -1) {
127
+ return dateLineIdx;
128
+ }
129
+ return promptLength;
130
+ }
131
+
132
+ // ---------------------------------------------------------------------------
133
+ // Main parser
134
+ // ---------------------------------------------------------------------------
135
+
136
+ /**
137
+ * Parse a system prompt string into sections with token estimates.
138
+ *
139
+ * Uses known structural markers emitted by `buildSystemPrompt()`:
140
+ * - `# Project Context` heading
141
+ * - `The following skills provide specialized instructions` preamble
142
+ * - `<available_skills>` / `</available_skills>` XML block
143
+ * - `Current date and time:` footer
144
+ */
145
+ export function parseSystemPrompt(prompt: string): ParsedPrompt {
146
+ const sections: PromptSection[] = [];
147
+ const skills: SkillEntry[] = [];
148
+
149
+ const projectCtxIdx = prompt.indexOf("\n\n# Project Context\n");
150
+ const skillsPreambleIdx = prompt.indexOf(
151
+ "\n\nThe following skills provide specialized instructions"
152
+ );
153
+ const availableSkillsStart = prompt.indexOf("<available_skills>");
154
+ const availableSkillsEnd = prompt.indexOf("</available_skills>");
155
+ const dateLineIdx = prompt.lastIndexOf("\nCurrent date and time:");
156
+
157
+ // 1. Base system prompt
158
+ const baseEnd = findBasePromptEnd(
159
+ prompt,
160
+ projectCtxIdx,
161
+ skillsPreambleIdx,
162
+ dateLineIdx
163
+ );
164
+ const baseText = baseEnd >= 0 ? prompt.slice(0, baseEnd) : prompt;
165
+ sections.push(measure("Base prompt", baseText));
166
+
167
+ // 2. Project Context / AGENTS.md files
168
+ if (projectCtxIdx !== -1) {
169
+ const contextStart = projectCtxIdx + 2; // skip leading \n\n
170
+ const contextEnd = firstPositive(skillsPreambleIdx, dateLineIdx);
171
+ const contextBlock =
172
+ contextEnd >= 0
173
+ ? prompt.slice(contextStart, contextEnd)
174
+ : prompt.slice(contextStart);
175
+
176
+ const agentsFiles = parseAgentsFiles(contextBlock);
177
+ const children = agentsFiles.map((f) => ({
178
+ label: f.path,
179
+ chars: f.chars,
180
+ tokens: f.tokens,
181
+ }));
182
+
183
+ sections.push({
184
+ ...measure("AGENTS.md files", contextBlock),
185
+ children,
186
+ });
187
+ }
188
+
189
+ // 3. Skills section
190
+ if (skillsPreambleIdx !== -1) {
191
+ const skillsSectionStart = skillsPreambleIdx + 2;
192
+ const skillsSectionEnd = findSkillsSectionEnd(
193
+ availableSkillsEnd,
194
+ dateLineIdx,
195
+ prompt.length
196
+ );
197
+ const skillsSectionText = prompt.slice(
198
+ skillsSectionStart,
199
+ skillsSectionEnd
200
+ );
201
+
202
+ if (availableSkillsStart !== -1 && availableSkillsEnd !== -1) {
203
+ const xmlBlock = prompt.slice(
204
+ availableSkillsStart,
205
+ availableSkillsEnd + "</available_skills>".length
206
+ );
207
+ parseSkillEntries(xmlBlock, skills);
208
+ }
209
+
210
+ const children = skills.map((s) => ({
211
+ label: s.name,
212
+ chars: s.chars,
213
+ tokens: s.tokens,
214
+ }));
215
+
216
+ sections.push({
217
+ ...measure(`Skills (${String(skills.length)})`, skillsSectionText),
218
+ children,
219
+ });
220
+ }
221
+
222
+ // 4. Metadata footer
223
+ if (dateLineIdx !== -1) {
224
+ const metaText = prompt.slice(dateLineIdx + 1);
225
+ sections.push(measure("Metadata (date/time, cwd)", metaText));
226
+ }
227
+
228
+ // 5. Detect SYSTEM.md / APPEND_SYSTEM.md gap
229
+ const nextSectionStart =
230
+ projectCtxIdx === -1 ? skillsPreambleIdx : projectCtxIdx;
231
+
232
+ if (baseEnd >= 0 && nextSectionStart >= 0 && nextSectionStart > baseEnd) {
233
+ const gap = prompt.slice(baseEnd, nextSectionStart);
234
+ const trimmed = gap.trim();
235
+ if (trimmed.length > 0) {
236
+ sections.splice(1, 0, measure("SYSTEM.md / APPEND_SYSTEM.md", trimmed));
237
+ }
238
+ }
239
+
240
+ const totalChars = prompt.length;
241
+ const totalTokens = estimateTokens(prompt);
242
+
243
+ return { sections, totalChars, totalTokens, skills };
244
+ }
@@ -0,0 +1,7 @@
1
+ import { showReport } from "./report-view.js";
2
+
3
+ describe("report-view", () => {
4
+ it("exports showReport function", () => {
5
+ expectTypeOf(showReport).toBeFunction();
6
+ });
7
+ });