chore: update tests, lockfiles, and misc cleanups

This commit is contained in:
can1357
2026-04-06 18:41:17 +02:00
parent 273369859b
commit 8718c62ea2
12 changed files with 243 additions and 60 deletions
+3
View File
@@ -56,3 +56,6 @@ pi-*.html
# Generated files
packages/coding-agent/src/internal-urls/docs-index.generated.ts
packages/typescript-edit-benchmark/runs/
all_models_results.json
+26 -26
View File
@@ -90,30 +90,6 @@
"@types/bun": "^1.3",
},
},
"packages/react-edit-benchmark": {
"name": "@oh-my-pi/react-edit-benchmark",
"version": "0.0.1",
"bin": {
"react-edit-benchmark": "src/index.ts",
},
"dependencies": {
"@babel/generator": "^7.29",
"@babel/parser": "^7.29",
"@babel/traverse": "^7.29",
"@babel/types": "^7.29",
"@oh-my-pi/pi-agent-core": "workspace:*",
"@oh-my-pi/pi-coding-agent": "workspace:*",
"@oh-my-pi/pi-utils": "workspace:*",
"diff": "^8.0",
"prettier": "^3.8",
"regexp-tree": "^0.1",
},
"devDependencies": {
"@types/babel__generator": "^7.27",
"@types/babel__traverse": "^7.28",
"@types/bun": "^1.3",
},
},
"packages/stats": {
"name": "@oh-my-pi/omp-stats",
"version": "13.19.0",
@@ -165,6 +141,30 @@
"chalk": "^5.6",
},
},
"packages/typescript-edit-benchmark": {
"name": "@oh-my-pi/typescript-edit-benchmark",
"version": "0.0.1",
"bin": {
"typescript-edit-benchmark": "src/index.ts",
},
"dependencies": {
"@babel/generator": "^7.29",
"@babel/parser": "^7.29",
"@babel/traverse": "^7.29",
"@babel/types": "^7.29",
"@oh-my-pi/pi-agent-core": "workspace:*",
"@oh-my-pi/pi-coding-agent": "workspace:*",
"@oh-my-pi/pi-utils": "workspace:*",
"diff": "^8.0",
"prettier": "^3.8",
"regexp-tree": "^0.1",
},
"devDependencies": {
"@types/babel__generator": "^7.27",
"@types/babel__traverse": "^7.28",
"@types/bun": "^1.3",
},
},
"packages/utils": {
"name": "@oh-my-pi/pi-utils",
"version": "13.19.0",
@@ -331,10 +331,10 @@
"@oh-my-pi/pi-utils": ["@oh-my-pi/pi-utils@workspace:packages/utils"],
"@oh-my-pi/react-edit-benchmark": ["@oh-my-pi/react-edit-benchmark@workspace:packages/react-edit-benchmark"],
"@oh-my-pi/swarm-extension": ["@oh-my-pi/swarm-extension@workspace:packages/swarm-extension"],
"@oh-my-pi/typescript-edit-benchmark": ["@oh-my-pi/typescript-edit-benchmark@workspace:packages/typescript-edit-benchmark"],
"@protobufjs/aspromise": ["@protobufjs/aspromise@1.1.2", "", {}, "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ=="],
"@protobufjs/base64": ["@protobufjs/base64@1.1.2", "", {}, "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg=="],
+2 -2
View File
@@ -24,8 +24,8 @@
"fix:rs": "cargo fmt --all && cargo clippy --fix --allow-dirty --all-targets --no-deps --allow-staged --broken-code --allow-no-vcs",
"build:native": "bun --cwd=packages/natives run build:native",
"dev:native": "bun --cwd=packages/natives run dev:native",
"bench:gen-fixtures": "bun --cwd=packages/react-edit-benchmark run src/generate.ts --react-dir /tmp/react-source --count-per-type 8",
"bench:edit": "bun --cwd=packages/react-edit-benchmark run start",
"bench:gen-fixtures": "bun --cwd=packages/typescript-edit-benchmark run src/generate.ts --typescript-dir /tmp/typescript-source --count-per-type 8",
"bench:edit": "bun --cwd=packages/typescript-edit-benchmark run start",
"prepublishOnly": "bun run check",
"prepare": "bun --cwd=packages/coding-agent run generate-docs-index",
"publish": "bun run prepublishOnly && npm publish -ws --access public",
+23 -1
View File
@@ -3,14 +3,36 @@
## [Unreleased]
### Added
- Autoresearch `init_experiment` options `from_autoresearch_md` (load contract from `autoresearch.md` with only `name`) and `abandon_unlogged_runs` (stamp pending run artifacts abandoned)
- Autoresearch `run_experiment` option `force` to override benchmark command equality and the direct-`autoresearch.sh` invocation rule (with warnings when used)
- `abandonUnloggedAutoresearchRuns` helper and `abandonedAt` run metadata so abandoned artifacts are ignored as pending runs
### Changed
- Autoresearch `log_experiment` refreshes benchmark/scope/constraints from `autoresearch.md` after resolving a pending run (so keep validation matches disk); success output includes a short refresh notice
- Autoresearch `log_experiment` `force` also skips ASI requirements and allows keeping a primary-metric regression versus the best kept run in the segment
### Added
- Support for LSP diagnostic versioning to track document versions and suppress stale diagnostics
- Options parameter to `waitForDiagnostics` for controlling diagnostic freshness validation with `expectedDocumentVersion` and `allowUnversioned` flags
### Changed
- `/autoresearch` with no `autoresearch.md` matches `/plan`-style flow: bare command enables mode and waits for the next composer message when off, or disables when already on; non-empty slash args are submitted as the user message only; setup protocol lives in the autoresearch system prompt (`command-initialize.md` removed)
- Enabled `versionSupport` in LSP client capabilities to receive diagnostic version information from servers
- Diagnostics storage now tracks both diagnostics and their associated document version for freshness validation
- Updated `getDiagnosticsForFile` to accept options object instead of positional parameters for better extensibility
- Redesigned chunk-mode `read` output to use a single recursive chunk rendering rule with `$XXXX` checksum suffixes and inline large-chunk previews
- Normalized copied `#chunk_path$XXXX` selectors across chunk-mode `read` and `edit` inputs so pasted chunk headers resolve without manual cleanup
### Fixed
- `/autoresearch` with no arguments toggles off when mode is already on (same idea as `/plan`), and slash-argument completion no longer offers `off`/`clear` on an empty prefix so Tab after the command does not auto-insert a subcommand.
- Fixed chunk-mode edit/read edge cases around zero-width gap splices, stale batch diagnostics, grouped Go receiver rendering, rendered line-count headers, and parse rejection location details.
### Removed
- Autoresearch segment fingerprint hashing and `segmentFingerprint` on experiment state / `autoresearch.jsonl` config lines
## [13.19.0] - 2026-04-05
### Added
@@ -6635,4 +6657,4 @@ Initial public release.
- Git branch display in footer
- Message queueing during streaming responses
- OAuth integration for Gmail and Google Calendar access
- HTML export with syntax highlighting and collapsible sections
- HTML export with syntax highlighting and collapsible sections
@@ -0,0 +1,126 @@
export type ExecutionAbortReason = "idle-timeout" | "signal";
export interface IdleTimeoutWatchdogOptions {
timeoutMs?: number;
signal?: AbortSignal;
hardTimeoutGraceMs: number;
onAbort?: (reason: ExecutionAbortReason) => void;
}
export class IdleTimeoutWatchdog {
#abortController = new AbortController();
#abortReason?: ExecutionAbortReason;
#hardTimeoutDeferred = Promise.withResolvers<"hard-timeout">();
#hardTimeoutGraceMs: number;
#hardTimeoutTimer?: NodeJS.Timeout;
#idleTimer?: NodeJS.Timeout;
#onAbort?: (reason: ExecutionAbortReason) => void;
#signal?: AbortSignal;
#signalAbortHandler?: () => void;
#timeoutMs?: number;
constructor(options: IdleTimeoutWatchdogOptions) {
this.#timeoutMs = options.timeoutMs;
this.#hardTimeoutGraceMs = options.hardTimeoutGraceMs;
this.#onAbort = options.onAbort;
this.#signal = options.signal;
if (this.#signal) {
if (this.#signal.aborted) {
this.#abort("signal");
return;
}
this.#signalAbortHandler = () => {
this.#abort("signal");
};
this.#signal.addEventListener("abort", this.#signalAbortHandler, { once: true });
}
this.touch();
}
get abortedBySignal(): boolean {
return this.#abortReason === "signal";
}
get hardTimeoutPromise(): Promise<"hard-timeout"> {
return this.#hardTimeoutDeferred.promise;
}
get signal(): AbortSignal {
return this.#abortController.signal;
}
get timedOut(): boolean {
return this.#abortReason === "idle-timeout";
}
touch(): void {
if (this.#abortReason || this.#timeoutMs === undefined || this.#timeoutMs <= 0) {
return;
}
if (this.#idleTimer) {
clearTimeout(this.#idleTimer);
}
this.#idleTimer = setTimeout(() => {
this.#abort("idle-timeout");
}, this.#timeoutMs);
}
dispose(): void {
if (this.#idleTimer) {
clearTimeout(this.#idleTimer);
this.#idleTimer = undefined;
}
if (this.#hardTimeoutTimer) {
clearTimeout(this.#hardTimeoutTimer);
this.#hardTimeoutTimer = undefined;
}
if (this.#signal && this.#signalAbortHandler) {
this.#signal.removeEventListener("abort", this.#signalAbortHandler);
this.#signalAbortHandler = undefined;
}
}
#abort(reason: ExecutionAbortReason): void {
if (this.#abortReason) {
return;
}
this.#abortReason = reason;
if (this.#idleTimer) {
clearTimeout(this.#idleTimer);
this.#idleTimer = undefined;
}
if (!this.#abortController.signal.aborted) {
this.#abortController.abort(reason);
}
this.#onAbort?.(reason);
this.#armHardTimeout();
}
#armHardTimeout(): void {
if (this.#hardTimeoutTimer || this.#hardTimeoutGraceMs <= 0) {
return;
}
this.#hardTimeoutTimer = setTimeout(() => {
this.#hardTimeoutDeferred.resolve("hard-timeout");
}, this.#hardTimeoutGraceMs);
}
}
export function formatIdleTimeoutMessage(timeoutMs?: number): string {
if (timeoutMs === undefined) {
return "Command timed out without output";
}
const seconds = Math.max(1, Math.round(timeoutMs / 1000));
return `Command timed out after ${seconds} seconds without output`;
}
@@ -1,10 +1,12 @@
import { invalidateFsScanCache } from "@oh-my-pi/pi-natives";
import { invalidateChunkTreeCache } from "./chunk-tree";
/**
* Invalidate shared filesystem scan caches after a content write/update.
*/
export function invalidateFsScanAfterWrite(path: string): void {
invalidateFsScanCache(path);
invalidateChunkTreeCache(path);
}
/**
@@ -12,6 +14,7 @@ export function invalidateFsScanAfterWrite(path: string): void {
*/
export function invalidateFsScanAfterDelete(path: string): void {
invalidateFsScanCache(path);
invalidateChunkTreeCache(path);
}
/**
@@ -22,7 +25,9 @@ export function invalidateFsScanAfterDelete(path: string): void {
*/
export function invalidateFsScanAfterRename(oldPath: string, newPath: string): void {
invalidateFsScanCache(oldPath);
invalidateChunkTreeCache(oldPath);
if (newPath !== oldPath) {
invalidateFsScanCache(newPath);
invalidateChunkTreeCache(newPath);
}
}
@@ -337,7 +337,7 @@ export function formatTruncationMetaNotice(truncation: TruncationMeta): string {
}
if (truncation.nextOffset != null) {
notice += `. Use offset=${truncation.nextOffset} to continue`;
notice += `. Use sel=L${truncation.nextOffset} to continue`;
}
if (truncation.artifactId != null) {
@@ -2,6 +2,8 @@
* Resolve line-display mode for file-like outputs (read, grep, @file mentions).
*/
import { resolveEditMode } from "./edit-mode";
export interface FileDisplayMode {
lineNumbers: boolean;
hashLines: boolean;
@@ -24,11 +26,7 @@ export interface FileDisplayModeSession {
export function resolveFileDisplayMode(session: FileDisplayModeSession): FileDisplayMode {
const { settings } = session;
const hasEditTool = session.hasEditTool ?? true;
const hashLines =
hasEditTool &&
(settings.get("readHashLines") === true ||
settings.get("edit.mode") === "hashline" ||
Bun.env.PI_EDIT_VARIANT === "hashline");
const hashLines = hasEditTool && resolveEditMode(session) === "hashline" && settings.get("readHashLines") !== false;
return {
hashLines,
lineNumbers: hashLines || settings.get("readLineNumbers") === true,
@@ -335,7 +335,7 @@ describe("truncation notice formatting", () => {
test("formatHeadTruncationNotice formats head truncation range", () => {
const lineTruncation = truncateHead("l1\nl2\nl3", { maxLines: 2, maxBytes: 100 });
expect(formatHeadTruncationNotice(lineTruncation)).toBe("\n\n[Showing lines 1-2 of 3. Use offset=3 to continue]");
expect(formatHeadTruncationNotice(lineTruncation)).toBe("\n\n[Showing lines 1-2 of 3. Use sel=L3 to continue]");
const byteTruncation = truncateHead("12345\nabc\nz", { maxLines: 10, maxBytes: 7 });
expect(
@@ -343,6 +343,6 @@ describe("truncation notice formatting", () => {
startLine: 100,
totalFileLines: 500,
}),
).toBe("\n\n[Showing lines 100-100 of 500. Use offset=101 to continue]");
).toBe("\n\n[Showing lines 100-100 of 500. Use sel=L101 to continue]");
});
});
+19 -19
View File
@@ -262,7 +262,7 @@ describe("Coding Agent Tools", () => {
expect(output).toContain("Line 2");
expect(output).toContain("Line 3");
// No truncation message since file fits within limits
expect(getTextOutput(result)).not.toContain("Use offset=");
expect(getTextOutput(result)).not.toContain("Use sel=");
expect(result.details?.truncation).toBeUndefined();
});
@@ -325,7 +325,7 @@ describe("Coding Agent Tools", () => {
expect(output).toContain(`Line ${defaultLimit}`);
expect(output).not.toContain(`Line ${defaultLimit + 1}`);
expect(output).toContain(
`[Showing lines 1-${defaultLimit} of 3500. Use offset=${defaultLimit + 1} to continue]`,
`[Showing lines 1-${defaultLimit} of 3500. Use sel=L${defaultLimit + 1} to continue]`,
);
});
@@ -341,7 +341,7 @@ describe("Coding Agent Tools", () => {
expect(output).toContain("Line 1:");
// Should show byte limit message
expect(output).toMatch(
/\[Showing lines 1-\d+ of 1000 \(\d+(\.\d+)?\s*KB limit\)\. Use offset=\d+ to continue\]/,
/\[Showing lines 1-\d+ of 1000 \(\d+(\.\d+)?\s*KB limit\)\. Use sel=L\d+ to continue\]/,
);
});
@@ -350,14 +350,14 @@ describe("Coding Agent Tools", () => {
const lines = Array.from({ length: 100 }, (_, i) => `Line ${i + 1}`);
fs.writeFileSync(testFile, lines.join("\n"));
const result = await readTool.execute("test-call-5", { path: testFile, offset: 51 });
const result = await readTool.execute("test-call-5", { path: testFile, sel: "L51" });
const output = getTextOutput(result);
expect(output).not.toContain("Line 50");
expect(output).toContain("Line 51");
expect(output).toContain("Line 100");
// No truncation message since file fits within limits
expect(output).not.toContain("Use offset=");
expect(output).not.toContain("Use sel=");
});
it("should handle limit parameter", async () => {
@@ -365,13 +365,13 @@ describe("Coding Agent Tools", () => {
const lines = Array.from({ length: 100 }, (_, i) => `Line ${i + 1}`);
fs.writeFileSync(testFile, lines.join("\n"));
const result = await readTool.execute("test-call-6", { path: testFile, limit: 10 });
const result = await readTool.execute("test-call-6", { path: testFile, sel: "L1-L10" });
const output = getTextOutput(result);
expect(output).toContain("Line 1");
expect(output).toContain("Line 10");
expect(output).not.toContain("Line 11");
expect(output).toContain("[Showing lines 1-10 of 100. Use offset=11 to continue]");
expect(output).toContain("[Showing lines 1-10 of 100. Use sel=L11 to continue]");
});
it("should handle offset + limit together", async () => {
@@ -381,8 +381,7 @@ describe("Coding Agent Tools", () => {
const result = await readTool.execute("test-call-7", {
path: testFile,
offset: 41,
limit: 20,
sel: "L41-L60",
});
const output = getTextOutput(result);
@@ -390,18 +389,18 @@ describe("Coding Agent Tools", () => {
expect(output).toContain("Line 41");
expect(output).toContain("Line 60");
expect(output).not.toContain("Line 61");
expect(output).toContain("[Showing lines 41-60 of 100. Use offset=61 to continue]");
expect(output).toContain("[Showing lines 41-60 of 100. Use sel=L61 to continue]");
});
it("should show error when offset is beyond file length", async () => {
const testFile = path.join(testDir, "short.txt");
fs.writeFileSync(testFile, "Line 1\nLine 2\nLine 3");
const result = await readTool.execute("test-call-8", { path: testFile, offset: 100 });
const result = await readTool.execute("test-call-8", { path: testFile, sel: "L100" });
const output = getTextOutput(result);
expect(output).toContain("Offset 100 is beyond end of file (3 lines total)");
expect(output).toContain("Use offset=1 to read from the start, or offset=3 to read the last line.");
expect(output).toContain("Line 100 is beyond end of file (3 lines total)");
expect(output).toContain("Use sel=L1 to read from the start, or sel=L3 to read the last line.");
});
it("should include truncation details when truncated", async () => {
@@ -492,14 +491,14 @@ describe("Coding Agent Tools", () => {
const result = await readTool.execute("test-call-archive-subpath", {
path: `${archivePath}:pkg/README.md`,
limit: 2,
sel: "L1-L2",
});
const output = getTextOutput(result);
expect(output).toContain("# Archive README");
expect(output).toContain("Line 2");
expect(output).not.toContain("Line 3");
expect(output).toContain("Use offset=3");
expect(output).toContain("Use sel=L3");
});
}
@@ -1010,7 +1009,8 @@ function b() {
const output = getTextOutput(result);
expect(output).not.toContain("# example.txt");
expect(output).toMatch(/>>\s*2#[ZPMQVRWSNKTXJBYH]{2}:match line/);
// PI_EDIT_VARIANT=replace in beforeEach disables hashlines; expect line-number mode
expect(output).toMatch(/\b2:match line/);
});
it("should accept wildcard patterns in the path parameter", async () => {
@@ -1069,9 +1069,9 @@ function b() {
const output = getTextOutput(result);
expect(output).not.toContain("# context.txt");
expect(output).toMatch(/\b1#[ZPMQVRWSNKTXJBYH]{2}:before/);
expect(output).toMatch(/>>\s*2#[ZPMQVRWSNKTXJBYH]{2}:match one/);
expect(output).toMatch(/\b3#[ZPMQVRWSNKTXJBYH]{2}:after/);
expect(output).toMatch(/\b1-before/);
expect(output).toMatch(/\b2:match one/);
expect(output).toMatch(/\b3-after/);
expect(output).toContain("[1 matches limit reached. Use limit=2 for more]");
// Ensure second match is not present
expect(output).not.toContain("match two");
@@ -261,7 +261,7 @@ describe("read tool URL handling", () => {
content: "<html><body>not really an image</body></html>",
});
const result = await tool.execute("fetch-html-png-path", { path: "https://example.com/foo.png", raw: true });
const result = await tool.execute("fetch-html-png-path", { path: "https://example.com/foo.png", sel: "raw" });
const imageBlock = result.content.find(content => content.type === "image");
const textBlock = result.content.find(content => content.type === "text");
@@ -603,8 +603,7 @@ describe("read tool URL handling", () => {
const pagedResult = await tool.execute("fetch-offset-page", {
path: pageUrl,
offset: 7,
limit: 2,
sel: "L7-L8",
});
const pagedText = pagedResult.content.find(content => content.type === "text");
expect(pagedText?.type).toBe("text");
@@ -1,11 +1,15 @@
import { describe, expect, it } from "bun:test";
import { describe, expect, it, vi } from "bun:test";
import * as fs from "node:fs";
import * as path from "node:path";
import type { RenderResultOptions } from "@oh-my-pi/pi-agent-core";
import { getServersForFile, loadConfig } from "@oh-my-pi/pi-coding-agent/lsp/config";
import { renderCall, renderResult } from "@oh-my-pi/pi-coding-agent/lsp/render";
import type { CodeAction, SymbolInformation } from "@oh-my-pi/pi-coding-agent/lsp/types";
import {
applyCodeAction,
collectGlobMatches,
dedupeWorkspaceSymbols,
detectLanguageId,
filterWorkspaceSymbols,
hasGlobPattern,
resolveSymbolColumn,
@@ -223,4 +227,30 @@ describe("lsp regressions", () => {
expect(normalizedResultText).toContain("occurrence: 2");
expect(resultText).not.toContain("\t");
});
it("detects tlaplus files for LSP startup and language ids", async () => {
const tempDir = TempDir.createSync("@omp-lsp-tlaplus-");
const specPath = path.join(tempDir.path(), "Spec.tla");
const aliasPath = path.join(tempDir.path(), "Spec.tlaplus");
await Bun.write(specPath, "---- MODULE Spec ----\n====\n");
const whichSpy = vi
.spyOn(Bun, "which")
.mockImplementation(command => (command === "tlapm_lsp" ? "/usr/local/bin/tlapm_lsp" : null));
const existsSpy = vi
.spyOn(fs, "existsSync")
.mockImplementation(candidate => typeof candidate === "string" && candidate === specPath);
try {
const config = loadConfig(tempDir.path());
expect(getServersForFile(config, specPath).map(([name]) => name)).toEqual(["tlaplus"]);
expect(whichSpy).toHaveBeenCalledWith("tlapm_lsp");
expect(existsSpy).toHaveBeenCalled();
expect(detectLanguageId(specPath)).toBe("tlaplus");
expect(detectLanguageId(aliasPath)).toBe("tlaplus");
} finally {
tempDir.removeSync();
}
});
});