@bugabinga/pi-ext-ctx 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/CHANGELOG.md +7 -0
- package/README.md +15 -0
- package/TODO.md +5 -0
- package/__tests__/ctx.test.ts +152 -0
- package/__tests__/harness.test.ts +296 -0
- package/assets/tools_suite.gif +0 -0
- package/bun.lock +300 -0
- package/index.ts +552 -0
- package/package.json +23 -0
package/CHANGELOG.md
ADDED
package/README.md
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# ctx
|
|
2
|
+
|
|
3
|
+
Off-context execution and large-output search for Pi.
|
|
4
|
+
|
|
5
|
+
Runs JS, Python, and shell analysis outside the main conversation and searches stored outputs.
|
|
6
|
+
|
|
7
|
+
## Tools
|
|
8
|
+
|
|
9
|
+
- `ctx`
|
|
10
|
+
|
|
11
|
+
## Demo
|
|
12
|
+
|
|
13
|
+
<!-- demo:tools_suite:start -->
|
|
14
|
+

|
|
15
|
+
<!-- demo:tools_suite:end -->
|
package/TODO.md
ADDED
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
|
|
2
|
+
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { CtxStore, defaultDbPath, executeCtxOperation, handleBashToolResult, registerCtxExtension, type CtxToolParams } from "../index.js";
|
|
6
|
+
|
|
7
|
+
let dir: string;
|
|
8
|
+
let dbPath: string;
|
|
9
|
+
|
|
10
|
+
beforeEach(() => {
|
|
11
|
+
dir = mkdtempSync(join(tmpdir(), "pi-ctx-test-"));
|
|
12
|
+
dbPath = join(dir, "ctx.db");
|
|
13
|
+
process.env.PI_CTX_DB_PATH = dbPath;
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
afterEach(() => {
|
|
17
|
+
delete process.env.PI_CTX_DB_PATH;
|
|
18
|
+
rmSync(dir, { recursive: true, force: true });
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
describe("ctx DB path", () => {
|
|
22
|
+
test("defaults under current Pi session dir", () => {
|
|
23
|
+
delete process.env.PI_CTX_DB_PATH;
|
|
24
|
+
const sessionFile = join(dir, "sessions", "session.jsonl");
|
|
25
|
+
const path = defaultDbPath("/repo/project", {
|
|
26
|
+
sessionManager: { getSessionFile: () => sessionFile },
|
|
27
|
+
});
|
|
28
|
+
expect(path).toStartWith(join(dir, "sessions", "ctx"));
|
|
29
|
+
expect(path).toEndWith(".db");
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
test("uses SessionManager sessionDir when no session file exists", () => {
|
|
33
|
+
delete process.env.PI_CTX_DB_PATH;
|
|
34
|
+
const path = defaultDbPath("/repo/project", {
|
|
35
|
+
sessionManager: { getSessionDir: () => join(dir, "session-dir") },
|
|
36
|
+
});
|
|
37
|
+
expect(path).toStartWith(join(dir, "session-dir", "ctx"));
|
|
38
|
+
expect(path).toEndWith(".db");
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
describe("CtxStore", () => {
|
|
43
|
+
test("indexes and searches chunks", () => {
|
|
44
|
+
const store = new CtxStore(dbPath);
|
|
45
|
+
const indexed = store.indexText({ kind: "test", label: "sample", text: "alpha beta\npanic stack trace\nomega" });
|
|
46
|
+
expect(indexed.chunkCount).toBeGreaterThan(0);
|
|
47
|
+
const hits = store.search(["panic trace"], { limit: 3 });
|
|
48
|
+
expect(hits).toContain("panic stack trace");
|
|
49
|
+
store.close();
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
test("purges project DB content", () => {
|
|
53
|
+
const store = new CtxStore(dbPath);
|
|
54
|
+
store.indexText({ kind: "test", label: "sample", text: "alpha beta" });
|
|
55
|
+
expect(store.stats().sources).toBe(1);
|
|
56
|
+
store.purge();
|
|
57
|
+
expect(store.stats().sources).toBe(0);
|
|
58
|
+
store.close();
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
describe("ctx operations", () => {
|
|
63
|
+
test("run-js returns printed findings", async () => {
|
|
64
|
+
const store = new CtxStore(dbPath);
|
|
65
|
+
const result = await executeCtxOperation(store, {
|
|
66
|
+
op: "run-js",
|
|
67
|
+
code: "console.log('answer', 41 + 1)",
|
|
68
|
+
} satisfies CtxToolParams, dir);
|
|
69
|
+
expect(result.isError).toBeFalsy();
|
|
70
|
+
expect(result.content[0].text).toContain("answer 42");
|
|
71
|
+
store.close();
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
test("file-py reads file as text", async () => {
|
|
75
|
+
const file = join(dir, "data.txt");
|
|
76
|
+
writeFileSync(file, "one\ntwo\nthree\n", "utf8");
|
|
77
|
+
const store = new CtxStore(dbPath);
|
|
78
|
+
const result = await executeCtxOperation(store, {
|
|
79
|
+
op: "file-py",
|
|
80
|
+
path: file,
|
|
81
|
+
code: "print(len(text.splitlines()))",
|
|
82
|
+
} satisfies CtxToolParams, dir);
|
|
83
|
+
expect(result.content[0].text.trim()).toBe("3");
|
|
84
|
+
store.close();
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
test("large run output is indexed and returns pointer", async () => {
|
|
88
|
+
const store = new CtxStore(dbPath);
|
|
89
|
+
const result = await executeCtxOperation(store, {
|
|
90
|
+
op: "run-js",
|
|
91
|
+
code: "console.log('x'.repeat(110_000))",
|
|
92
|
+
} satisfies CtxToolParams, dir);
|
|
93
|
+
expect(result.content[0].text).toContain("ctx:stored");
|
|
94
|
+
expect(result.content[0].text).toContain("Use ctx search");
|
|
95
|
+
expect(store.stats().sources).toBe(1);
|
|
96
|
+
store.close();
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
describe("bash auto-collapse", () => {
|
|
101
|
+
test("uses fullOutputPath, stores raw, returns bounded replacement", () => {
|
|
102
|
+
const full = join(dir, "bash.log");
|
|
103
|
+
const raw = "CTX_RAW_SENTINEL ".repeat(10_000);
|
|
104
|
+
writeFileSync(full, raw, "utf8");
|
|
105
|
+
const store = new CtxStore(dbPath);
|
|
106
|
+
const patch = handleBashToolResult(store, {
|
|
107
|
+
toolName: "bash",
|
|
108
|
+
input: { command: "python noisy.py" },
|
|
109
|
+
content: [{ type: "text", text: "[truncated]" }],
|
|
110
|
+
details: { fullOutputPath: full, truncation: { truncated: true } },
|
|
111
|
+
isError: false,
|
|
112
|
+
});
|
|
113
|
+
expect(patch).toBeDefined();
|
|
114
|
+
const text = patch!.content[0].text;
|
|
115
|
+
expect(text).toContain("ctx:stored");
|
|
116
|
+
expect(text).not.toContain("CTX_RAW_SENTINEL");
|
|
117
|
+
expect(store.search(["CTX_RAW_SENTINEL"], { limit: 1 })).toContain("CTX_RAW_SENTINEL");
|
|
118
|
+
store.close();
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
test("large error output is searched for failures", () => {
|
|
122
|
+
const full = join(dir, "bash-error.log");
|
|
123
|
+
writeFileSync(full, `${"noise\n".repeat(1000)}panic: exploded\nstack trace here\n`, "utf8");
|
|
124
|
+
const store = new CtxStore(dbPath);
|
|
125
|
+
const patch = handleBashToolResult(store, {
|
|
126
|
+
toolName: "bash",
|
|
127
|
+
input: { command: "failing" },
|
|
128
|
+
content: [{ type: "text", text: "[truncated]" }],
|
|
129
|
+
details: { fullOutputPath: full },
|
|
130
|
+
isError: true,
|
|
131
|
+
});
|
|
132
|
+
expect(patch?.content[0].text).toContain("matched");
|
|
133
|
+
expect(patch?.content[0].text).toContain("panic");
|
|
134
|
+
store.close();
|
|
135
|
+
});
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
describe("extension registration", () => {
|
|
139
|
+
test("registers one tool and one command", () => {
|
|
140
|
+
const tools: any[] = [];
|
|
141
|
+
const commands: Record<string, any> = {};
|
|
142
|
+
const handlers: Record<string, Function[]> = {};
|
|
143
|
+
registerCtxExtension({
|
|
144
|
+
registerTool: (tool: any) => tools.push(tool),
|
|
145
|
+
registerCommand: (name: string, command: any) => { commands[name] = command; },
|
|
146
|
+
on: (name: string, handler: Function) => { (handlers[name] ??= []).push(handler); },
|
|
147
|
+
});
|
|
148
|
+
expect(tools.map(t => t.name)).toEqual(["ctx"]);
|
|
149
|
+
expect(Object.keys(commands)).toEqual(["ctx"]);
|
|
150
|
+
expect(Object.keys(handlers)).toContain("tool_result");
|
|
151
|
+
});
|
|
152
|
+
});
|
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it } from "bun:test";
|
|
2
|
+
import { existsSync, mkdtempSync, readdirSync, readFileSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import path, { join } from "node:path";
|
|
5
|
+
import { createTestSession, type TestSession } from "@marcfargas/pi-test-harness";
|
|
6
|
+
|
|
7
|
+
const CTX_COMMIT = "0cb5ce5f10fb18c9a80e904e2e3310cc98a28e29";
|
|
8
|
+
|
|
9
|
+
let root: string;
|
|
10
|
+
let sessionDir: string;
|
|
11
|
+
let t: TestSession | undefined;
|
|
12
|
+
|
|
13
|
+
function ctxPath(): string {
|
|
14
|
+
return path.resolve(import.meta.dir, "../index.ts");
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function ctxTool() {
|
|
18
|
+
return t!.session.extensionRunner.getToolDefinition("ctx");
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function ctxExec(params: Record<string, unknown>) {
|
|
22
|
+
return ctxTool().execute(
|
|
23
|
+
`call-${crypto.randomUUID()}`,
|
|
24
|
+
params,
|
|
25
|
+
undefined,
|
|
26
|
+
undefined,
|
|
27
|
+
t!.session.extensionRunner.createContext(),
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function textOf(result: any): string {
|
|
32
|
+
return result.content.map((part: { text?: string }) => part.text ?? "").join("\n");
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function dbFiles(): string[] {
|
|
36
|
+
const dir = join(sessionDir, "ctx");
|
|
37
|
+
if (!existsSync(dir)) return [];
|
|
38
|
+
return readdirSync(dir).filter((name) => name.endsWith(".db"));
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
beforeEach(async () => {
|
|
42
|
+
root = mkdtempSync(join(tmpdir(), "pi-ctx-harness-"));
|
|
43
|
+
sessionDir = join(root, "session");
|
|
44
|
+
process.env.PI_CODING_AGENT_SESSION_DIR = sessionDir;
|
|
45
|
+
delete process.env.PI_CTX_DB_PATH;
|
|
46
|
+
t = await createTestSession({ extensions: [ctxPath()] });
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
afterEach(() => {
|
|
50
|
+
t?.dispose();
|
|
51
|
+
t = undefined;
|
|
52
|
+
delete process.env.PI_CODING_AGENT_SESSION_DIR;
|
|
53
|
+
delete process.env.PI_CTX_DB_PATH;
|
|
54
|
+
rmSync(root, { recursive: true, force: true });
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
describe("ctx harness integration", () => {
|
|
58
|
+
it("loads from git pi-test-harness fork pinned to source-export commit", async () => {
|
|
59
|
+
const pkg = await import("@marcfargas/pi-test-harness");
|
|
60
|
+
expect(typeof pkg.createTestSession).toBe("function");
|
|
61
|
+
const lock = readFileSync(path.resolve(import.meta.dir, "../bun.lock"), "utf8");
|
|
62
|
+
expect(lock).toContain(CTX_COMMIT);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it("registers exactly one ctx tool and one ctx command", () => {
|
|
66
|
+
const [extension] = t!.session.extensionRunner.extensions;
|
|
67
|
+
expect([...extension.tools.keys()]).toEqual(["ctx"]);
|
|
68
|
+
expect([...extension.commands.keys()]).toEqual(["ctx"]);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it("runs JS off-context, stores oversized output, and retrieves exact needles later", async () => {
|
|
72
|
+
const result = await ctxExec({
|
|
73
|
+
op: "run-js",
|
|
74
|
+
query: "ctxneedle12345",
|
|
75
|
+
code: `
|
|
76
|
+
for (let i = 0; i < 9000; i++) console.log('noise line ' + i + ' ' + 'x'.repeat(20));
|
|
77
|
+
console.log('ctxneedle12345 fatal failure in generated report');
|
|
78
|
+
for (let i = 0; i < 9000; i++) console.log('tail noise ' + i + ' ' + 'y'.repeat(20));
|
|
79
|
+
`,
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
const text = textOf(result);
|
|
83
|
+
expect(text).toContain("ctx:stored");
|
|
84
|
+
expect(text).toContain("matched");
|
|
85
|
+
expect(text.length).toBeLessThan(2_000);
|
|
86
|
+
expect(text).not.toContain("tail noise 8999");
|
|
87
|
+
expect(dbFiles()).toHaveLength(1);
|
|
88
|
+
|
|
89
|
+
const search = await ctxExec({ op: "search", query: "ctxneedle12345", limit: 1 });
|
|
90
|
+
const searchText = textOf(search);
|
|
91
|
+
expect(searchText).toContain("ctxneedle12345 fatal failure");
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it("analyzes a huge file without returning the file body or indexing small findings", async () => {
|
|
95
|
+
const file = join(root, "huge.log");
|
|
96
|
+
const lines = Array.from({ length: 25_000 }, (_, i) => `row ${i} boring payload ${"z".repeat(40)}`);
|
|
97
|
+
lines[19_876] = "ctxfilemarker67890 ERROR customer=42 shard=west";
|
|
98
|
+
writeFileSync(file, `${lines.join("\n")}\n`, "utf8");
|
|
99
|
+
|
|
100
|
+
const result = await ctxExec({
|
|
101
|
+
op: "file-js",
|
|
102
|
+
path: file,
|
|
103
|
+
code: `
|
|
104
|
+
const hit = text.split(/\\n/).find(line => line.includes('ctxfilemarker67890'));
|
|
105
|
+
console.log(hit);
|
|
106
|
+
`,
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
const out = textOf(result);
|
|
110
|
+
expect(out.trim()).toBe("ctxfilemarker67890 ERROR customer=42 shard=west");
|
|
111
|
+
expect(out).not.toContain("row 0 boring payload");
|
|
112
|
+
const [extension] = t!.session.extensionRunner.extensions;
|
|
113
|
+
const stats = await extension.commands.get("ctx").handler("", t!.session.extensionRunner.createContext());
|
|
114
|
+
expect(stats.text).toContain("ctx: 0 sources");
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it("collapses huge bash tool_result using fullOutputPath before it reaches the transcript", async () => {
|
|
118
|
+
const raw = [
|
|
119
|
+
"header",
|
|
120
|
+
...Array.from({ length: 5 }, (_, i) => `bash noise ${i} ${"a".repeat(30)}`),
|
|
121
|
+
"bashneedle24680 terminal fact hidden in full log",
|
|
122
|
+
...Array.from({ length: 7_500 }, (_, i) => `bash tail ${i} ${"a".repeat(30)}`),
|
|
123
|
+
].join("\n");
|
|
124
|
+
const full = join(root, "bash-full.log");
|
|
125
|
+
writeFileSync(full, raw, "utf8");
|
|
126
|
+
|
|
127
|
+
const patched = await t!.session.extensionRunner.emitToolResult({
|
|
128
|
+
type: "tool_result",
|
|
129
|
+
toolName: "bash",
|
|
130
|
+
input: { command: "generate massive report" },
|
|
131
|
+
content: [{ type: "text", text: "[pi truncated visible output]" }],
|
|
132
|
+
details: { fullOutputPath: full, truncation: { truncated: true } },
|
|
133
|
+
isError: false,
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
const text = textOf(patched);
|
|
137
|
+
expect(text).toContain("ctx:stored");
|
|
138
|
+
expect(text).toContain("Use ctx search");
|
|
139
|
+
expect(text).not.toContain("bashneedle24680");
|
|
140
|
+
expect(patched.details.ctxStored).toBe(true);
|
|
141
|
+
expect(statSync(join(sessionDir, "ctx", dbFiles()[0])).size).toBeGreaterThan(0);
|
|
142
|
+
|
|
143
|
+
const search = await ctxExec({ op: "search", query: "bashneedle24680", limit: 1 });
|
|
144
|
+
expect(textOf(search)).toContain("bashneedle24680 terminal fact");
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it("extracts searchable failure context for huge failing bash output and keeps isError", async () => {
|
|
148
|
+
const full = join(root, "bash-error.log");
|
|
149
|
+
writeFileSync(full, `${"boring\n".repeat(5_000)}panic ctxboom999 exploded\nstack trace line\n`, "utf8");
|
|
150
|
+
|
|
151
|
+
const patched = await t!.session.extensionRunner.emitToolResult({
|
|
152
|
+
type: "tool_result",
|
|
153
|
+
toolName: "bash",
|
|
154
|
+
input: { command: "run failing suite" },
|
|
155
|
+
content: [{ type: "text", text: "[truncated]" }],
|
|
156
|
+
details: { fullOutputPath: full },
|
|
157
|
+
isError: true,
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
expect(patched.isError).toBe(true);
|
|
161
|
+
expect(textOf(patched)).toContain("ctx:stored");
|
|
162
|
+
expect(textOf(patched)).toContain("matched");
|
|
163
|
+
|
|
164
|
+
const search = await ctxExec({ op: "search", query: "ctxboom999", limit: 1 });
|
|
165
|
+
expect(textOf(search)).toContain("panic ctxboom999 exploded");
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
it("search returns the matching region when a hit is buried deep inside a chunk", async () => {
|
|
169
|
+
const filler = Array.from({ length: 180 }, (_, i) => `preface ${i} ${"p".repeat(80)}`).join("\n");
|
|
170
|
+
await ctxExec({
|
|
171
|
+
op: "run-js",
|
|
172
|
+
query: "buriedneedle424242",
|
|
173
|
+
code: `console.log(${JSON.stringify(`${filler}\nburiedneedle424242 exact match near end of chunk\n${"tail ".repeat(5_000)}`)})`,
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
const search = await ctxExec({ op: "search", query: "buriedneedle424242", limit: 1 });
|
|
177
|
+
const out = textOf(search);
|
|
178
|
+
expect(out).toContain("buriedneedle424242 exact match");
|
|
179
|
+
expect(out.length).toBeLessThan(3_000);
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
it("collapses actual built-in bash execution output using Pi fullOutputPath metadata", async () => {
|
|
183
|
+
const bash = (t!.session.agent as any).state.tools.find((tool: any) => tool.name === "bash");
|
|
184
|
+
expect(bash).toBeDefined();
|
|
185
|
+
const command = `python3 - <<'PY'\nfor i in range(7000): print(f'realbashneedle777 line {i}')\nPY`;
|
|
186
|
+
const raw = await bash.execute("bash-real-1", { command }, undefined, undefined, t!.session.extensionRunner.createContext());
|
|
187
|
+
expect(typeof raw.details?.fullOutputPath).toBe("string");
|
|
188
|
+
expect(raw.details?.truncation?.truncated).toBe(true);
|
|
189
|
+
|
|
190
|
+
try {
|
|
191
|
+
const patched = await t!.session.extensionRunner.emitToolResult({
|
|
192
|
+
type: "tool_result",
|
|
193
|
+
toolCallId: "bash-real-1",
|
|
194
|
+
toolName: "bash",
|
|
195
|
+
input: { command },
|
|
196
|
+
content: raw.content,
|
|
197
|
+
details: raw.details,
|
|
198
|
+
isError: raw.isError,
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
expect(textOf(patched)).toContain("ctx:stored");
|
|
202
|
+
expect(textOf(patched)).not.toContain("realbashneedle777 line 6999");
|
|
203
|
+
const search = await ctxExec({ op: "search", query: "realbashneedle777", limit: 1 });
|
|
204
|
+
expect(textOf(search)).toContain("realbashneedle777 line");
|
|
205
|
+
} finally {
|
|
206
|
+
rmSync(raw.details?.fullOutputPath, { force: true });
|
|
207
|
+
}
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
it("falls back to visible bash content when fullOutputPath is absent", async () => {
|
|
211
|
+
const visible = `${"visible filler\n".repeat(9_000)}visibleonly333 final line`;
|
|
212
|
+
const patched = await t!.session.extensionRunner.emitToolResult({
|
|
213
|
+
type: "tool_result",
|
|
214
|
+
toolCallId: "bash-visible-1",
|
|
215
|
+
toolName: "bash",
|
|
216
|
+
input: { command: "already truncated elsewhere" },
|
|
217
|
+
content: [{ type: "text", text: visible }],
|
|
218
|
+
details: { truncation: { truncated: true } },
|
|
219
|
+
isError: false,
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
expect(textOf(patched)).toContain("ctx:stored");
|
|
223
|
+
expect(textOf(patched)).not.toContain("visibleonly333");
|
|
224
|
+
const search = await ctxExec({ op: "search", query: "visibleonly333", limit: 1 });
|
|
225
|
+
expect(textOf(search)).toContain("visibleonly333 final line");
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
it("handles concurrent ctx writes without lock errors or lost sources", async () => {
|
|
229
|
+
const count = 8;
|
|
230
|
+
const results = await Promise.all(Array.from({ length: count }, (_, i) => ctxExec({
|
|
231
|
+
op: "run-js",
|
|
232
|
+
code: `console.log('concurrentneedle${i} ' + '${"x".repeat(130_000)}')`,
|
|
233
|
+
})));
|
|
234
|
+
|
|
235
|
+
for (const result of results) expect(textOf(result)).toContain("ctx:stored");
|
|
236
|
+
const [extension] = t!.session.extensionRunner.extensions;
|
|
237
|
+
const stats = await extension.commands.get("ctx").handler("", t!.session.extensionRunner.createContext());
|
|
238
|
+
expect(stats.text).toContain(`ctx: ${count} sources`);
|
|
239
|
+
|
|
240
|
+
for (let i = 0; i < count; i++) {
|
|
241
|
+
const search = await ctxExec({ op: "search", query: `concurrentneedle${i}`, limit: 1 });
|
|
242
|
+
expect(textOf(search)).toContain(`concurrentneedle${i}`);
|
|
243
|
+
}
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
it("keeps unicode output valid while using byte thresholds conservatively", async () => {
|
|
247
|
+
const marker = "unicodeNeedle555 測試 ✅";
|
|
248
|
+
const result = await ctxExec({
|
|
249
|
+
op: "run-js",
|
|
250
|
+
code: `console.log('${"😀".repeat(40_000)} ${marker}')`,
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
expect(textOf(result)).toContain("ctx:stored");
|
|
254
|
+
const search = await ctxExec({ op: "search", query: "unicodeNeedle555", limit: 1 });
|
|
255
|
+
expect(textOf(search)).toContain(marker);
|
|
256
|
+
expect(textOf(search)).not.toContain("�");
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
it("closes ctx resources so session directory can be removed after dispose", async () => {
|
|
260
|
+
await ctxExec({ op: "run-js", code: "console.log('cleanupneedle ' + 'x'.repeat(120_000))" });
|
|
261
|
+
expect(dbFiles()).toHaveLength(1);
|
|
262
|
+
t!.dispose();
|
|
263
|
+
t = undefined;
|
|
264
|
+
rmSync(root, { recursive: true, force: true });
|
|
265
|
+
expect(existsSync(root)).toBe(false);
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
it("does not touch small bash output, then /ctx reports, searches, and purges stored data", async () => {
|
|
269
|
+
const unchanged = await t!.session.extensionRunner.emitToolResult({
|
|
270
|
+
type: "tool_result",
|
|
271
|
+
toolName: "bash",
|
|
272
|
+
input: { command: "echo small" },
|
|
273
|
+
content: [{ type: "text", text: "small output" }],
|
|
274
|
+
details: {},
|
|
275
|
+
isError: false,
|
|
276
|
+
});
|
|
277
|
+
expect(unchanged).toBeUndefined();
|
|
278
|
+
|
|
279
|
+
await ctxExec({ op: "run-js", code: "console.log('cmdneedle13579 ' + 'q'.repeat(120_000))" });
|
|
280
|
+
const [extension] = t!.session.extensionRunner.extensions;
|
|
281
|
+
const command = extension.commands.get("ctx");
|
|
282
|
+
|
|
283
|
+
const stats = await command.handler("", t!.session.extensionRunner.createContext());
|
|
284
|
+
expect(stats.text).toContain("ctx: 1 sources");
|
|
285
|
+
expect(stats.text).toContain("recent:");
|
|
286
|
+
|
|
287
|
+
const found = await command.handler("cmdneedle13579", t!.session.extensionRunner.createContext());
|
|
288
|
+
expect(found.text).toContain("cmdneedle13579");
|
|
289
|
+
|
|
290
|
+
const purged = await command.handler("--purge", t!.session.extensionRunner.createContext());
|
|
291
|
+
expect(purged.text).toBe("ctx purged");
|
|
292
|
+
|
|
293
|
+
const empty = await command.handler("", t!.session.extensionRunner.createContext());
|
|
294
|
+
expect(empty.text).toContain("ctx: 0 sources");
|
|
295
|
+
});
|
|
296
|
+
});
|
|
Binary file
|