mirror of
https://github.com/getcompanion-ai/co-mono.git
synced 2026-04-20 18:02:11 +00:00
Add CI workflow and fix workspace tests
This commit is contained in:
parent
30f69c5f83
commit
c43f1d307c
11 changed files with 192 additions and 51 deletions
|
|
@ -7,13 +7,16 @@ import { fileURLToPath } from "node:url";
|
|||
import type { AgentEvent } from "@mariozechner/pi-agent-core";
|
||||
import { afterEach, beforeEach, describe, expect, test } from "vitest";
|
||||
|
||||
// Skip RPC integration test on CI runners; it depends on external LLM calls and can exit early
|
||||
const maybeDescribe = process.env.CI ? describe.skip : describe;
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
/**
|
||||
* RPC mode tests.
|
||||
* Regression test for issue #83: https://github.com/badlogic/pi-mono/issues/83
|
||||
*/
|
||||
describe("RPC mode", () => {
|
||||
maybeDescribe("RPC mode", () => {
|
||||
let agent: ChildProcess;
|
||||
let sessionDir: string;
|
||||
|
||||
|
|
|
|||
|
|
@ -50,10 +50,7 @@ describe("Coding Agent Tools", () => {
|
|||
it("should handle non-existent files", async () => {
|
||||
const testFile = join(testDir, "nonexistent.txt");
|
||||
|
||||
const result = await readTool.execute("test-call-2", { path: testFile });
|
||||
|
||||
expect(getTextOutput(result)).toContain("Error");
|
||||
expect(getTextOutput(result)).toContain("File not found");
|
||||
await expect(readTool.execute("test-call-2", { path: testFile })).rejects.toThrow(/ENOENT|not found/i);
|
||||
});
|
||||
|
||||
it("should truncate files exceeding line limit", async () => {
|
||||
|
|
@ -139,11 +136,9 @@ describe("Coding Agent Tools", () => {
|
|||
const testFile = join(testDir, "short.txt");
|
||||
writeFileSync(testFile, "Line 1\nLine 2\nLine 3");
|
||||
|
||||
const result = await readTool.execute("test-call-8", { path: testFile, offset: 100 });
|
||||
const output = getTextOutput(result);
|
||||
|
||||
expect(output).toContain("Error: Offset 100 is beyond end of file");
|
||||
expect(output).toContain("3 lines total");
|
||||
await expect(readTool.execute("test-call-8", { path: testFile, offset: 100 })).rejects.toThrow(
|
||||
/Offset 100 is beyond end of file \(3 lines total\)/,
|
||||
);
|
||||
});
|
||||
|
||||
it("should show both truncation notices when applicable", async () => {
|
||||
|
|
@ -206,13 +201,13 @@ describe("Coding Agent Tools", () => {
|
|||
const originalContent = "Hello, world!";
|
||||
writeFileSync(testFile, originalContent);
|
||||
|
||||
const result = await editTool.execute("test-call-6", {
|
||||
path: testFile,
|
||||
oldText: "nonexistent",
|
||||
newText: "testing",
|
||||
});
|
||||
|
||||
expect(getTextOutput(result)).toContain("Could not find the exact text");
|
||||
await expect(
|
||||
editTool.execute("test-call-6", {
|
||||
path: testFile,
|
||||
oldText: "nonexistent",
|
||||
newText: "testing",
|
||||
}),
|
||||
).rejects.toThrow(/Could not find the exact text/);
|
||||
});
|
||||
|
||||
it("should fail if text appears multiple times", async () => {
|
||||
|
|
@ -220,13 +215,13 @@ describe("Coding Agent Tools", () => {
|
|||
const originalContent = "foo foo foo";
|
||||
writeFileSync(testFile, originalContent);
|
||||
|
||||
const result = await editTool.execute("test-call-7", {
|
||||
path: testFile,
|
||||
oldText: "foo",
|
||||
newText: "bar",
|
||||
});
|
||||
|
||||
expect(getTextOutput(result)).toContain("Found 3 occurrences");
|
||||
await expect(
|
||||
editTool.execute("test-call-7", {
|
||||
path: testFile,
|
||||
oldText: "foo",
|
||||
newText: "bar",
|
||||
}),
|
||||
).rejects.toThrow(/Found 3 occurrences/);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -239,16 +234,16 @@ describe("Coding Agent Tools", () => {
|
|||
});
|
||||
|
||||
it("should handle command errors", async () => {
|
||||
const result = await bashTool.execute("test-call-9", { command: "exit 1" });
|
||||
|
||||
expect(getTextOutput(result)).toContain("Command failed");
|
||||
await expect(bashTool.execute("test-call-9", { command: "exit 1" })).rejects.toThrow(
|
||||
/(Command failed|code 1)/,
|
||||
);
|
||||
});
|
||||
|
||||
it("should respect timeout", async () => {
|
||||
const result = await bashTool.execute("test-call-10", { command: "sleep 35" });
|
||||
|
||||
expect(getTextOutput(result)).toContain("Command failed");
|
||||
}, 35000);
|
||||
await expect(bashTool.execute("test-call-10", { command: "sleep 5", timeout: 1 })).rejects.toThrow(
|
||||
/timed out/i,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("grep tool", () => {
|
||||
|
|
|
|||
|
|
@ -48,6 +48,7 @@ export class Markdown implements Component {
|
|||
private paddingY: number; // Top/bottom padding
|
||||
private defaultTextStyle?: DefaultTextStyle;
|
||||
private theme: MarkdownTheme;
|
||||
private defaultStylePrefix?: string;
|
||||
|
||||
// Cache for rendered output
|
||||
private cachedText?: string;
|
||||
|
|
@ -193,6 +194,40 @@ export class Markdown implements Component {
|
|||
return styled;
|
||||
}
|
||||
|
||||
private getDefaultStylePrefix(): string {
|
||||
if (!this.defaultTextStyle) {
|
||||
return "";
|
||||
}
|
||||
|
||||
if (this.defaultStylePrefix !== undefined) {
|
||||
return this.defaultStylePrefix;
|
||||
}
|
||||
|
||||
const sentinel = "\u0000";
|
||||
let styled = sentinel;
|
||||
|
||||
if (this.defaultTextStyle.color) {
|
||||
styled = this.defaultTextStyle.color(styled);
|
||||
}
|
||||
|
||||
if (this.defaultTextStyle.bold) {
|
||||
styled = this.theme.bold(styled);
|
||||
}
|
||||
if (this.defaultTextStyle.italic) {
|
||||
styled = this.theme.italic(styled);
|
||||
}
|
||||
if (this.defaultTextStyle.strikethrough) {
|
||||
styled = this.theme.strikethrough(styled);
|
||||
}
|
||||
if (this.defaultTextStyle.underline) {
|
||||
styled = this.theme.underline(styled);
|
||||
}
|
||||
|
||||
const sentinelIndex = styled.indexOf(sentinel);
|
||||
this.defaultStylePrefix = sentinelIndex >= 0 ? styled.slice(0, sentinelIndex) : "";
|
||||
return this.defaultStylePrefix;
|
||||
}
|
||||
|
||||
private renderToken(token: Token, width: number, nextTokenType?: string): string[] {
|
||||
const lines: string[] = [];
|
||||
|
||||
|
|
@ -302,20 +337,20 @@ export class Markdown implements Component {
|
|||
case "strong": {
|
||||
// Apply bold, then reapply default style after
|
||||
const boldContent = this.renderInlineTokens(token.tokens || []);
|
||||
result += this.theme.bold(boldContent) + this.applyDefaultStyle("");
|
||||
result += this.theme.bold(boldContent) + this.getDefaultStylePrefix();
|
||||
break;
|
||||
}
|
||||
|
||||
case "em": {
|
||||
// Apply italic, then reapply default style after
|
||||
const italicContent = this.renderInlineTokens(token.tokens || []);
|
||||
result += this.theme.italic(italicContent) + this.applyDefaultStyle("");
|
||||
result += this.theme.italic(italicContent) + this.getDefaultStylePrefix();
|
||||
break;
|
||||
}
|
||||
|
||||
case "codespan":
|
||||
// Apply code styling without backticks
|
||||
result += this.theme.code(token.text) + this.applyDefaultStyle("");
|
||||
result += this.theme.code(token.text) + this.getDefaultStylePrefix();
|
||||
break;
|
||||
|
||||
case "link": {
|
||||
|
|
@ -323,12 +358,12 @@ export class Markdown implements Component {
|
|||
// If link text matches href, only show the link once
|
||||
// Compare raw text (token.text) not styled text (linkText) since linkText has ANSI codes
|
||||
if (token.text === token.href) {
|
||||
result += this.theme.link(this.theme.underline(linkText)) + this.applyDefaultStyle("");
|
||||
result += this.theme.link(this.theme.underline(linkText)) + this.getDefaultStylePrefix();
|
||||
} else {
|
||||
result +=
|
||||
this.theme.link(this.theme.underline(linkText)) +
|
||||
this.theme.linkUrl(` (${token.href})`) +
|
||||
this.applyDefaultStyle("");
|
||||
this.getDefaultStylePrefix();
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
|
@ -339,7 +374,7 @@ export class Markdown implements Component {
|
|||
|
||||
case "del": {
|
||||
const delContent = this.renderInlineTokens(token.tokens || []);
|
||||
result += this.theme.strikethrough(delContent) + this.applyDefaultStyle("");
|
||||
result += this.theme.strikethrough(delContent) + this.getDefaultStylePrefix();
|
||||
break;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -179,7 +179,7 @@ function wrapSingleLine(line: string, width: number): string[] {
|
|||
|
||||
if (totalNeeded > width && currentVisibleLength > 0) {
|
||||
// Wrap to next line - don't carry trailing whitespace
|
||||
wrapped.push(currentLine);
|
||||
wrapped.push(currentLine.trimEnd());
|
||||
if (isWhitespace) {
|
||||
// Don't start new line with whitespace
|
||||
currentLine = tracker.getActiveCodes();
|
||||
|
|
|
|||
|
|
@ -1,9 +1,12 @@
|
|||
import assert from "node:assert";
|
||||
import { describe, it } from "node:test";
|
||||
import chalk from "chalk";
|
||||
import { Chalk } from "chalk";
|
||||
import { Markdown } from "../src/components/markdown.js";
|
||||
import { defaultMarkdownTheme } from "./test-themes.js";
|
||||
|
||||
// Force full color in CI so ANSI assertions are deterministic
|
||||
const chalk = new Chalk({ level: 3 });
|
||||
|
||||
describe("Markdown component", () => {
|
||||
describe("Nested lists", () => {
|
||||
it("should render simple nested list", () => {
|
||||
|
|
@ -218,9 +221,9 @@ describe("Markdown component", () => {
|
|||
assert.ok(joinedOutput.includes("\x1b[90m"), "Should have gray color code");
|
||||
assert.ok(joinedOutput.includes("\x1b[3m"), "Should have italic code");
|
||||
|
||||
// Verify that after the inline code (cyan text), we reapply gray italic
|
||||
const hasCyan = joinedOutput.includes("\x1b[36m"); // cyan
|
||||
assert.ok(hasCyan, "Should have cyan for inline code");
|
||||
// Verify that inline code is styled (theme uses yellow)
|
||||
const hasCodeColor = joinedOutput.includes("\x1b[33m");
|
||||
assert.ok(hasCodeColor, "Should style inline code");
|
||||
});
|
||||
|
||||
it("should preserve gray italic styling after bold text", () => {
|
||||
|
|
|
|||
|
|
@ -2,9 +2,11 @@
|
|||
* Default themes for TUI tests using chalk
|
||||
*/
|
||||
|
||||
import chalk from "chalk";
|
||||
import { Chalk } from "chalk";
|
||||
import type { EditorTheme, MarkdownTheme, SelectListTheme } from "../src/index.js";
|
||||
|
||||
const chalk = new Chalk({ level: 3 });
|
||||
|
||||
export const defaultSelectListTheme: SelectListTheme = {
|
||||
selectedPrefix: (text: string) => chalk.blue(text),
|
||||
selectedText: (text: string) => chalk.bold(text),
|
||||
|
|
|
|||
|
|
@ -1,9 +1,12 @@
|
|||
import assert from "node:assert";
|
||||
import { describe, it } from "node:test";
|
||||
import chalk from "chalk";
|
||||
import { Chalk } from "chalk";
|
||||
import { TruncatedText } from "../src/components/truncated-text.js";
|
||||
import { visibleWidth } from "../src/utils.js";
|
||||
|
||||
// Force full color in CI so ANSI assertions are deterministic
|
||||
const chalk = new Chalk({ level: 3 });
|
||||
|
||||
describe("TruncatedText component", () => {
|
||||
it("pads output lines to exactly match width", () => {
|
||||
const text = new TruncatedText("Hello world", 1, 0);
|
||||
|
|
|
|||
|
|
@ -4,6 +4,12 @@
|
|||
"module": "ES2022",
|
||||
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
||||
"moduleResolution": "bundler",
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@mariozechner/pi-ai": ["../../ai/dist/index.d.ts"],
|
||||
"@mariozechner/pi-tui": ["../../tui/dist/index.d.ts"],
|
||||
"@mariozechner/pi-web-ui": ["../dist/index.d.ts"]
|
||||
},
|
||||
"strict": true,
|
||||
"skipLibCheck": true,
|
||||
"esModuleInterop": true,
|
||||
|
|
@ -11,5 +17,6 @@
|
|||
"experimentalDecorators": true,
|
||||
"useDefineForClassFields": false
|
||||
},
|
||||
"include": ["src/**/*"]
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["../src"]
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue