diff --git a/packages/opencode/src/acp/agent.ts b/packages/opencode/src/acp/agent.ts index 030b2d7d26..c7631def61 100644 --- a/packages/opencode/src/acp/agent.ts +++ b/packages/opencode/src/acp/agent.ts @@ -136,6 +136,8 @@ export namespace ACP { private eventAbort = new AbortController() private eventStarted = false private permissionQueues = new Map>() + private seenTools = new Set() + private bashSnapshots = new Map() private permissionOptions: PermissionOption[] = [ { optionId: "once", kind: "allow_once", name: "Allow once" }, { optionId: "always", kind: "allow_always", name: "Always allow" }, @@ -288,6 +290,8 @@ export namespace ACP { if (part.type === "tool") { switch (part.state.status) { case "pending": + this.seenTools.add(part.callID) + this.bashSnapshots.delete(part.callID) await this.connection .sessionUpdate({ sessionId, @@ -306,7 +310,62 @@ export namespace ACP { }) return - case "running": + case "running": { + // Emit synthetic pending if we haven't seen this tool yet + if (!this.seenTools.has(part.callID)) { + this.seenTools.add(part.callID) + await this.connection + .sessionUpdate({ + sessionId, + update: { + sessionUpdate: "tool_call", + toolCallId: part.callID, + title: part.tool, + kind: toToolKind(part.tool), + status: "pending", + locations: [], + rawInput: {}, + }, + }) + .catch((error) => { + log.error("failed to send synthetic tool pending to ACP", { error }) + }) + } + + // Extract bash output from metadata for streaming + const metadata = part.state.metadata as Record | undefined + const bashOutput = metadata && typeof metadata.output === "string" ? metadata.output : undefined + const content: ToolCallContent[] = [] + + if (bashOutput !== undefined) { + const lastSnapshot = this.bashSnapshots.get(part.callID) + if (lastSnapshot === bashOutput) { + // De-duplicate: identical to last snapshot, send update without content + await this.connection + .sessionUpdate({ + sessionId, + update: { + sessionUpdate: "tool_call_update", + toolCallId: part.callID, + status: "in_progress", + kind: toToolKind(part.tool), + title: part.tool, + locations: toLocations(part.tool, part.state.input), + rawInput: part.state.input, + }, + }) + .catch((error) => { + log.error("failed to send tool in_progress to ACP", { error }) + }) + return + } + this.bashSnapshots.set(part.callID, bashOutput) + content.push({ + type: "content", + content: { type: "text", text: bashOutput }, + }) + } + await this.connection .sessionUpdate({ sessionId, @@ -318,12 +377,14 @@ export namespace ACP { title: part.tool, locations: toLocations(part.tool, part.state.input), rawInput: part.state.input, + ...(content.length > 0 && { content }), }, }) .catch((error) => { log.error("failed to send tool in_progress to ACP", { error }) }) return + } case "completed": { const kind = toToolKind(part.tool) @@ -802,6 +863,8 @@ export namespace ACP { if (part.type === "tool") { switch (part.state.status) { case "pending": + this.seenTools.add(part.callID) + this.bashSnapshots.delete(part.callID) await this.connection .sessionUpdate({ sessionId, @@ -819,7 +882,60 @@ export namespace ACP { log.error("failed to send tool pending to ACP", { error: err }) }) break - case "running": + case "running": { + // Emit synthetic pending if we haven't seen this tool yet + if (!this.seenTools.has(part.callID)) { + this.seenTools.add(part.callID) + await this.connection + .sessionUpdate({ + sessionId, + update: { + sessionUpdate: "tool_call", + toolCallId: part.callID, + title: part.tool, + kind: toToolKind(part.tool), + status: "pending", + locations: [], + rawInput: {}, + }, + }) + .catch((err) => { + log.error("failed to send synthetic tool pending to ACP", { error: err }) + }) + } + + const metadata = part.state.metadata as Record | undefined + const bashOutput = metadata && typeof metadata.output === "string" ? metadata.output : undefined + const content: ToolCallContent[] = [] + + if (bashOutput !== undefined) { + const lastSnapshot = this.bashSnapshots.get(part.callID) + if (lastSnapshot === bashOutput) { + await this.connection + .sessionUpdate({ + sessionId, + update: { + sessionUpdate: "tool_call_update", + toolCallId: part.callID, + status: "in_progress", + kind: toToolKind(part.tool), + title: part.tool, + locations: toLocations(part.tool, part.state.input), + rawInput: part.state.input, + }, + }) + .catch((err) => { + log.error("failed to send tool in_progress to ACP", { error: err }) + }) + break + } + this.bashSnapshots.set(part.callID, bashOutput) + content.push({ + type: "content", + content: { type: "text", text: bashOutput }, + }) + } + await this.connection .sessionUpdate({ sessionId, @@ -831,12 +947,14 @@ export namespace ACP { title: part.tool, locations: toLocations(part.tool, part.state.input), rawInput: part.state.input, + ...(content.length > 0 && { content }), }, }) .catch((err) => { log.error("failed to send tool in_progress to ACP", { error: err }) }) break + } case "completed": const kind = toToolKind(part.tool) const content: ToolCallContent[] = [ diff --git a/packages/opencode/src/provider/error.ts b/packages/opencode/src/provider/error.ts index 945d29f97f..82fcb18368 100644 --- a/packages/opencode/src/provider/error.ts +++ b/packages/opencode/src/provider/error.ts @@ -20,6 +20,8 @@ export namespace ProviderError { /exceeded model token limit/i, // Kimi For Coding, Moonshot /context[_ ]length[_ ]exceeded/i, // Generic fallback /request entity too large/i, // HTTP 413 + /the request was too long/i, // Azure OpenAI + /maximum tokens for requested operation/i, // Azure OpenAI ] function isOpenAiErrorRetryable(e: APICallError) { diff --git a/packages/opencode/test/acp/event-subscription.test.ts b/packages/opencode/test/acp/event-subscription.test.ts index 1abf578281..41020b7b65 100644 --- a/packages/opencode/test/acp/event-subscription.test.ts +++ b/packages/opencode/test/acp/event-subscription.test.ts @@ -224,8 +224,8 @@ function createFakeAgent() { return { data: [ { - name: "build", - description: "build", + name: "builder", + description: "builder", mode: "agent", }, ], diff --git a/packages/opencode/test/agent/agent.test.ts b/packages/opencode/test/agent/agent.test.ts index 9f8de04f80..890ce6e005 100644 --- a/packages/opencode/test/agent/agent.test.ts +++ b/packages/opencode/test/agent/agent.test.ts @@ -18,28 +18,32 @@ test("returns default native agents when no config", async () => { fn: async () => { const agents = await Agent.list() const names = agents.map((a) => a.name) - expect(names).toContain("build") + expect(names).toContain("builder") expect(names).toContain("plan") expect(names).toContain("general") expect(names).toContain("explore") expect(names).toContain("compaction") expect(names).toContain("title") expect(names).toContain("summary") + expect(names).toContain("analyst") + expect(names).toContain("executive") + expect(names).toContain("validator") + expect(names).toContain("migrator") }, }) }) -test("build agent has correct default properties", async () => { +test("builder agent has correct default properties", async () => { await using tmp = await tmpdir() await Instance.provide({ directory: tmp.path, fn: async () => { - const build = await Agent.get("build") - expect(build).toBeDefined() - expect(build?.mode).toBe("primary") - expect(build?.native).toBe(true) - expect(evalPerm(build, "edit")).toBe("allow") - expect(evalPerm(build, "bash")).toBe("allow") + const builder = await Agent.get("builder") + expect(builder).toBeDefined() + expect(builder?.mode).toBe("primary") + expect(builder?.native).toBe(true) + expect(evalPerm(builder, "edit")).toBe("allow") + expect(evalPerm(builder, "bash")).toBe("allow") }, }) }) @@ -152,9 +156,9 @@ test("custom agent config overrides native agent properties", async () => { await using tmp = await tmpdir({ config: { agent: { - build: { + builder: { model: "anthropic/claude-3", - description: "Custom build agent", + description: "Custom builder agent", temperature: 0.7, color: "#FF0000", }, @@ -164,14 +168,14 @@ test("custom agent config overrides native agent properties", async () => { await Instance.provide({ directory: tmp.path, fn: async () => { - const build = await Agent.get("build") - expect(build).toBeDefined() - expect(build?.model?.providerID).toBe("anthropic") - expect(build?.model?.modelID).toBe("claude-3") - expect(build?.description).toBe("Custom build agent") - expect(build?.temperature).toBe(0.7) - expect(build?.color).toBe("#FF0000") - expect(build?.native).toBe(true) + const builder = await Agent.get("builder") + expect(builder).toBeDefined() + expect(builder?.model?.providerID).toBe("anthropic") + expect(builder?.model?.modelID).toBe("claude-3") + expect(builder?.description).toBe("Custom builder agent") + expect(builder?.temperature).toBe(0.7) + expect(builder?.color).toBe("#FF0000") + expect(builder?.native).toBe(true) }, }) }) @@ -200,7 +204,7 @@ test("agent permission config merges with defaults", async () => { await using tmp = await tmpdir({ config: { agent: { - build: { + builder: { permission: { bash: { "rm -rf *": "deny", @@ -213,12 +217,12 @@ test("agent permission config merges with defaults", async () => { await Instance.provide({ directory: tmp.path, fn: async () => { - const build = await Agent.get("build") - expect(build).toBeDefined() + const builder = await Agent.get("builder") + expect(builder).toBeDefined() // Specific pattern is denied - expect(PermissionNext.evaluate("bash", "rm -rf *", build!.permission).action).toBe("deny") + expect(PermissionNext.evaluate("bash", "rm -rf *", builder!.permission).action).toBe("deny") // Edit still allowed - expect(evalPerm(build, "edit")).toBe("allow") + expect(evalPerm(builder, "edit")).toBe("allow") }, }) }) @@ -234,9 +238,9 @@ test("global permission config applies to all agents", async () => { await Instance.provide({ directory: tmp.path, fn: async () => { - const build = await Agent.get("build") - expect(build).toBeDefined() - expect(evalPerm(build, "bash")).toBe("deny") + const builder = await Agent.get("builder") + expect(builder).toBeDefined() + expect(evalPerm(builder, "bash")).toBe("deny") }, }) }) @@ -245,7 +249,7 @@ test("agent steps/maxSteps config sets steps property", async () => { await using tmp = await tmpdir({ config: { agent: { - build: { steps: 50 }, + builder: { steps: 50 }, plan: { maxSteps: 100 }, }, }, @@ -253,9 +257,9 @@ test("agent steps/maxSteps config sets steps property", async () => { await Instance.provide({ directory: tmp.path, fn: async () => { - const build = await Agent.get("build") + const builder = await Agent.get("builder") const plan = await Agent.get("plan") - expect(build?.steps).toBe(50) + expect(builder?.steps).toBe(50) expect(plan?.steps).toBe(100) }, }) @@ -282,15 +286,15 @@ test("agent name can be overridden", async () => { await using tmp = await tmpdir({ config: { agent: { - build: { name: "Builder" }, + builder: { name: "CustomBuilder" }, }, }, }) await Instance.provide({ directory: tmp.path, fn: async () => { - const build = await Agent.get("build") - expect(build?.name).toBe("Builder") + const builder = await Agent.get("builder") + expect(builder?.name).toBe("CustomBuilder") }, }) }) @@ -299,15 +303,15 @@ test("agent prompt can be set from config", async () => { await using tmp = await tmpdir({ config: { agent: { - build: { prompt: "Custom system prompt" }, + builder: { prompt: "Custom system prompt" }, }, }, }) await Instance.provide({ directory: tmp.path, fn: async () => { - const build = await Agent.get("build") - expect(build?.prompt).toBe("Custom system prompt") + const builder = await Agent.get("builder") + expect(builder?.prompt).toBe("Custom system prompt") }, }) }) @@ -316,7 +320,7 @@ test("unknown agent properties are placed into options", async () => { await using tmp = await tmpdir({ config: { agent: { - build: { + builder: { random_property: "hello", another_random: 123, }, @@ -326,9 +330,9 @@ test("unknown agent properties are placed into options", async () => { await Instance.provide({ directory: tmp.path, fn: async () => { - const build = await Agent.get("build") - expect(build?.options.random_property).toBe("hello") - expect(build?.options.another_random).toBe(123) + const builder = await Agent.get("builder") + expect(builder?.options.random_property).toBe("hello") + expect(builder?.options.another_random).toBe(123) }, }) }) @@ -337,7 +341,7 @@ test("agent options merge correctly", async () => { await using tmp = await tmpdir({ config: { agent: { - build: { + builder: { options: { custom_option: true, another_option: "value", @@ -349,9 +353,9 @@ test("agent options merge correctly", async () => { await Instance.provide({ directory: tmp.path, fn: async () => { - const build = await Agent.get("build") - expect(build?.options.custom_option).toBe(true) - expect(build?.options.another_option).toBe("value") + const builder = await Agent.get("builder") + expect(builder?.options.custom_option).toBe(true) + expect(builder?.options.another_option).toBe("value") }, }) }) @@ -400,9 +404,9 @@ test("default permission includes doom_loop and external_directory as ask", asyn await Instance.provide({ directory: tmp.path, fn: async () => { - const build = await Agent.get("build") - expect(evalPerm(build, "doom_loop")).toBe("ask") - expect(evalPerm(build, "external_directory")).toBe("ask") + const builder = await Agent.get("builder") + expect(evalPerm(builder, "doom_loop")).toBe("ask") + expect(evalPerm(builder, "external_directory")).toBe("ask") }, }) }) @@ -412,8 +416,8 @@ test("webfetch is allowed by default", async () => { await Instance.provide({ directory: tmp.path, fn: async () => { - const build = await Agent.get("build") - expect(evalPerm(build, "webfetch")).toBe("allow") + const builder = await Agent.get("builder") + expect(evalPerm(builder, "webfetch")).toBe("allow") }, }) }) @@ -422,7 +426,7 @@ test("legacy tools config converts to permissions", async () => { await using tmp = await tmpdir({ config: { agent: { - build: { + builder: { tools: { bash: false, read: false, @@ -434,9 +438,9 @@ test("legacy tools config converts to permissions", async () => { await Instance.provide({ directory: tmp.path, fn: async () => { - const build = await Agent.get("build") - expect(evalPerm(build, "bash")).toBe("deny") - expect(evalPerm(build, "read")).toBe("deny") + const builder = await Agent.get("builder") + expect(evalPerm(builder, "bash")).toBe("deny") + expect(evalPerm(builder, "read")).toBe("deny") }, }) }) @@ -445,7 +449,7 @@ test("legacy tools config maps write/edit/patch/multiedit to edit permission", a await using tmp = await tmpdir({ config: { agent: { - build: { + builder: { tools: { write: false, }, @@ -456,8 +460,8 @@ test("legacy tools config maps write/edit/patch/multiedit to edit permission", a await Instance.provide({ directory: tmp.path, fn: async () => { - const build = await Agent.get("build") - expect(evalPerm(build, "edit")).toBe("deny") + const builder = await Agent.get("builder") + expect(evalPerm(builder, "edit")).toBe("deny") }, }) }) @@ -474,10 +478,10 @@ test("Truncate.GLOB is allowed even when user denies external_directory globally await Instance.provide({ directory: tmp.path, fn: async () => { - const build = await Agent.get("build") - expect(PermissionNext.evaluate("external_directory", Truncate.GLOB, build!.permission).action).toBe("allow") - expect(PermissionNext.evaluate("external_directory", Truncate.DIR, build!.permission).action).toBe("deny") - expect(PermissionNext.evaluate("external_directory", "/some/other/path", build!.permission).action).toBe("deny") + const builder = await Agent.get("builder") + expect(PermissionNext.evaluate("external_directory", Truncate.GLOB, builder!.permission).action).toBe("allow") + expect(PermissionNext.evaluate("external_directory", Truncate.DIR, builder!.permission).action).toBe("deny") + expect(PermissionNext.evaluate("external_directory", "/some/other/path", builder!.permission).action).toBe("deny") }, }) }) @@ -487,7 +491,7 @@ test("Truncate.GLOB is allowed even when user denies external_directory per-agen await using tmp = await tmpdir({ config: { agent: { - build: { + builder: { permission: { external_directory: "deny", }, @@ -498,10 +502,10 @@ test("Truncate.GLOB is allowed even when user denies external_directory per-agen await Instance.provide({ directory: tmp.path, fn: async () => { - const build = await Agent.get("build") - expect(PermissionNext.evaluate("external_directory", Truncate.GLOB, build!.permission).action).toBe("allow") - expect(PermissionNext.evaluate("external_directory", Truncate.DIR, build!.permission).action).toBe("deny") - expect(PermissionNext.evaluate("external_directory", "/some/other/path", build!.permission).action).toBe("deny") + const builder = await Agent.get("builder") + expect(PermissionNext.evaluate("external_directory", Truncate.GLOB, builder!.permission).action).toBe("allow") + expect(PermissionNext.evaluate("external_directory", Truncate.DIR, builder!.permission).action).toBe("deny") + expect(PermissionNext.evaluate("external_directory", "/some/other/path", builder!.permission).action).toBe("deny") }, }) }) @@ -521,9 +525,9 @@ test("explicit Truncate.GLOB deny is respected", async () => { await Instance.provide({ directory: tmp.path, fn: async () => { - const build = await Agent.get("build") - expect(PermissionNext.evaluate("external_directory", Truncate.GLOB, build!.permission).action).toBe("deny") - expect(PermissionNext.evaluate("external_directory", Truncate.DIR, build!.permission).action).toBe("deny") + const builder = await Agent.get("builder") + expect(PermissionNext.evaluate("external_directory", Truncate.GLOB, builder!.permission).action).toBe("deny") + expect(PermissionNext.evaluate("external_directory", Truncate.DIR, builder!.permission).action).toBe("deny") }, }) }) @@ -553,10 +557,10 @@ description: Permission skill. await Instance.provide({ directory: tmp.path, fn: async () => { - const build = await Agent.get("build") + const builder = await Agent.get("builder") const skillDir = path.join(tmp.path, ".opencode", "skill", "perm-skill") const target = path.join(skillDir, "reference", "notes.md") - expect(PermissionNext.evaluate("external_directory", target, build!.permission).action).toBe("allow") + expect(PermissionNext.evaluate("external_directory", target, builder!.permission).action).toBe("allow") }, }) } finally { @@ -564,13 +568,13 @@ description: Permission skill. } }) -test("defaultAgent returns build when no default_agent config", async () => { +test("defaultAgent returns builder when no default_agent config", async () => { await using tmp = await tmpdir() await Instance.provide({ directory: tmp.path, fn: async () => { const agent = await Agent.defaultAgent() - expect(agent).toBe("build") + expect(agent).toBe("builder") }, }) }) @@ -652,11 +656,11 @@ test("defaultAgent throws when default_agent points to non-existent agent", asyn }) }) -test("defaultAgent returns plan when build is disabled and default_agent not set", async () => { +test("defaultAgent returns analyst when builder is disabled and default_agent not set", async () => { await using tmp = await tmpdir({ config: { agent: { - build: { disable: true }, + builder: { disable: true }, }, }, }) @@ -664,8 +668,8 @@ test("defaultAgent returns plan when build is disabled and default_agent not set directory: tmp.path, fn: async () => { const agent = await Agent.defaultAgent() - // build is disabled, so it should return plan (next primary agent) - expect(agent).toBe("plan") + // builder is disabled, so it should return the next primary agent + expect(agent).toBe("analyst") }, }) }) @@ -674,7 +678,11 @@ test("defaultAgent throws when all primary agents are disabled", async () => { await using tmp = await tmpdir({ config: { agent: { - build: { disable: true }, + builder: { disable: true }, + analyst: { disable: true }, + executive: { disable: true }, + validator: { disable: true }, + migrator: { disable: true }, plan: { disable: true }, }, }, @@ -682,7 +690,7 @@ test("defaultAgent throws when all primary agents are disabled", async () => { await Instance.provide({ directory: tmp.path, fn: async () => { - // build and plan are disabled, no primary-capable agents remain + // All primary visible agents are disabled await expect(Agent.defaultAgent()).rejects.toThrow("no primary visible agent found") }, }) diff --git a/packages/opencode/test/bridge/client.test.ts b/packages/opencode/test/bridge/client.test.ts index 13431cf892..3676711390 100644 --- a/packages/opencode/test/bridge/client.test.ts +++ b/packages/opencode/test/bridge/client.test.ts @@ -16,7 +16,7 @@ let managedPythonPath = "/nonexistent/managed-engine/venv/bin/python" // Mock: bridge/engine (only module we mock — avoids leaking into other tests) // --------------------------------------------------------------------------- -mock.module("../../src/bridge/engine", () => ({ +mock.module("../../src/altimate/bridge/engine", () => ({ ensureEngine: async () => { ensureEngineCalls++ }, @@ -27,7 +27,7 @@ mock.module("../../src/bridge/engine", () => ({ // Import module under test — AFTER mock.module() calls // --------------------------------------------------------------------------- -const { resolvePython } = await import("../../src/bridge/client") +const { resolvePython } = await import("../../src/altimate/bridge/client") // --------------------------------------------------------------------------- // Helpers @@ -138,7 +138,7 @@ describe("Bridge.start integration", () => { }) test("ensureEngine is called when bridge starts", async () => { - const { Bridge } = await import("../../src/bridge/client") + const { Bridge } = await import("../../src/altimate/bridge/client") // /bin/echo exists and will spawn successfully but won't respond to // the JSON-RPC ping, so start() will eventually fail on verification. diff --git a/packages/opencode/test/bridge/engine.test.ts b/packages/opencode/test/bridge/engine.test.ts index 954c0cb2b3..83ef352d0c 100644 --- a/packages/opencode/test/bridge/engine.test.ts +++ b/packages/opencode/test/bridge/engine.test.ts @@ -95,7 +95,7 @@ describe("engine.ts subprocess noise suppression", () => { // is actually applied in the production code const engineSrc = path.resolve( __dirname, - "../../src/bridge/engine.ts", + "../../src/altimate/bridge/engine.ts", ) const source = await fsp.readFile(engineSrc, "utf-8") const lines = source.split("\n") diff --git a/packages/opencode/test/install/bin-wrapper.test.ts b/packages/opencode/test/install/bin-wrapper.test.ts index ceafd4cf96..344d67e43d 100644 --- a/packages/opencode/test/install/bin-wrapper.test.ts +++ b/packages/opencode/test/install/bin-wrapper.test.ts @@ -41,13 +41,13 @@ describe("bin/altimate-code wrapper", () => { expect(result.stdout).toContain("altimate-code-test-ok") }) - test("uses cached .opencode when present", () => { + test("uses cached .altimate-code when present", () => { const { dir, cleanup: c } = installTmpdir() cleanup = c const wrapperPath = copyBinWrapper(dir) const binDir = path.dirname(wrapperPath) - createDummyBinary(binDir, ".opencode") + createDummyBinary(binDir, ".altimate-code") const result = runBinWrapper(wrapperPath) expect(result.exitCode).toBe(0) @@ -59,8 +59,8 @@ describe("bin/altimate-code wrapper", () => { cleanup = c // Standard npm flat layout: - // dir/node_modules/@opencode-ai/opencode/bin/altimate-code (wrapper) - // dir/node_modules/@opencode-ai/opencode-{p}-{a}/bin/binary (binary) + // dir/node_modules/@altimateai/altimate-code/bin/altimate (wrapper) + // dir/node_modules/@altimateai/altimate-code-{p}-{a}/bin/binary (binary) const wrapperPkgBin = path.join(dir, "node_modules", "@altimateai", "altimate-code", "bin") fs.mkdirSync(wrapperPkgBin, { recursive: true }) const wrapperPath = path.join(wrapperPkgBin, "altimate-code") @@ -78,8 +78,8 @@ describe("bin/altimate-code wrapper", () => { cleanup = c // Hoisted layout: - // dir/node_modules/@opencode-ai/opencode-{p}-{a}/bin/binary (hoisted binary) - // dir/packages/app/node_modules/@opencode-ai/opencode/bin/wrapper + // dir/node_modules/@altimateai/altimate-code-{p}-{a}/bin/binary (hoisted binary) + // dir/packages/app/node_modules/@altimateai/altimate-code/bin/wrapper createBinaryPackage(dir) const nestedBin = path.join(dir, "packages", "app", "node_modules", "@altimateai", "altimate-code", "bin") @@ -111,6 +111,6 @@ describe("bin/altimate-code wrapper", () => { const result = runBinWrapper(wrapperPath) expect(result.exitCode).toBe(1) - expect(result.stderr).toContain(`@opencode-ai/opencode-${CURRENT_PLATFORM}-${CURRENT_ARCH}`) + expect(result.stderr).toContain(`@altimateai/altimate-code-${CURRENT_PLATFORM}-${CURRENT_ARCH}`) }) }) diff --git a/packages/opencode/test/install/fixture.ts b/packages/opencode/test/install/fixture.ts index b289f07df8..2992165f56 100644 --- a/packages/opencode/test/install/fixture.ts +++ b/packages/opencode/test/install/fixture.ts @@ -8,12 +8,12 @@ const ARCH_MAP: Record = { x64: "x64", arm64: "arm64", arm: "arm export const CURRENT_PLATFORM = PLATFORM_MAP[os.platform()] ?? os.platform() export const CURRENT_ARCH = ARCH_MAP[os.arch()] ?? os.arch() -export const CURRENT_PKG_NAME = `@opencode-ai/opencode-${CURRENT_PLATFORM}-${CURRENT_ARCH}` +export const CURRENT_PKG_NAME = `@altimateai/altimate-code-${CURRENT_PLATFORM}-${CURRENT_ARCH}` export const BINARY_NAME = CURRENT_PLATFORM === "windows" ? "altimate-code.exe" : "altimate-code" const REPO_PKG_DIR = path.resolve(import.meta.dir, "../..") export const POSTINSTALL_SCRIPT = path.join(REPO_PKG_DIR, "script/postinstall.mjs") -export const BIN_WRAPPER_SCRIPT = path.join(REPO_PKG_DIR, "bin/altimate-code") +export const BIN_WRAPPER_SCRIPT = path.join(REPO_PKG_DIR, "bin/altimate") export function installTmpdir(): { dir: string; cleanup: () => void } { const dir = fs.mkdtempSync(path.join(os.tmpdir(), "altimate-install-test-")) @@ -37,7 +37,7 @@ export function createMainPackageDir(baseDir: string, opts?: MainPackageOpts) { fs.writeFileSync( path.join(baseDir, "package.json"), - JSON.stringify({ name: "@opencode-ai/opencode", version }, null, 2), + JSON.stringify({ name: "@altimateai/altimate-code", version }, null, 2), ) if (!opts?.noBinDir) { @@ -54,7 +54,7 @@ interface BinaryPackageOpts { export function createBinaryPackage(baseDir: string, opts?: BinaryPackageOpts) { const platform = opts?.platform ?? CURRENT_PLATFORM const arch = opts?.arch ?? CURRENT_ARCH - const pkgName = `@opencode-ai/opencode-${platform}-${arch}` + const pkgName = `@altimateai/altimate-code-${platform}-${arch}` const binaryName = platform === "windows" ? "altimate-code.exe" : "altimate-code" const pkgDir = path.join(baseDir, "node_modules", "@altimateai", `altimate-code-${platform}-${arch}`) diff --git a/packages/opencode/test/install/integration.test.ts b/packages/opencode/test/install/integration.test.ts index 81aa425921..4328d44990 100644 --- a/packages/opencode/test/install/integration.test.ts +++ b/packages/opencode/test/install/integration.test.ts @@ -27,18 +27,18 @@ describe("install pipeline integration", () => { createMainPackageDir(dir) createBinaryPackage(dir) - // 2. Postinstall creates .opencode hard link + // 2. Postinstall creates .altimate-code hard link const postResult = runPostinstall(dir) expect(postResult.exitCode).toBe(0) - const cachedBin = path.join(dir, "bin", ".opencode") + const cachedBin = path.join(dir, "bin", ".altimate-code") expect(fs.existsSync(cachedBin)).toBe(true) // 3. Place bin wrapper in the same bin/ directory const wrapperPath = path.join(dir, "bin", "altimate-code") fs.copyFileSync(BIN_WRAPPER_SCRIPT, wrapperPath) - // 4. Wrapper finds cached .opencode and executes it + // 4. Wrapper finds cached .altimate-code and executes it const wrapperResult = runBinWrapper(wrapperPath) expect(wrapperResult.exitCode).toBe(0) expect(wrapperResult.stdout).toContain("altimate-code-test-ok") @@ -57,7 +57,7 @@ describe("install pipeline integration", () => { expect(postResult.stderr).toContain("Failed to setup altimate-code binary") // 2. No cached binary was created - expect(fs.existsSync(path.join(dir, "bin", ".opencode"))).toBe(false) + expect(fs.existsSync(path.join(dir, "bin", ".altimate-code"))).toBe(false) // 3. Bin wrapper also fails with helpful error when invoked directly const wrapperPkgBin = path.join(dir, "node_modules", "@altimateai", "altimate-code", "bin") diff --git a/packages/opencode/test/install/postinstall.test.ts b/packages/opencode/test/install/postinstall.test.ts index b9239ecf77..0ceea15f1f 100644 --- a/packages/opencode/test/install/postinstall.test.ts +++ b/packages/opencode/test/install/postinstall.test.ts @@ -27,7 +27,7 @@ describe("postinstall.mjs", () => { const result = runPostinstall(dir) expect(result.exitCode).toBe(0) - const cachedBinary = path.join(dir, "bin", ".opencode") + const cachedBinary = path.join(dir, "bin", ".altimate-code") expect(fs.existsSync(cachedBinary)).toBe(true) // Verify it's executable const stat = fs.statSync(cachedBinary) @@ -41,8 +41,8 @@ describe("postinstall.mjs", () => { createMainPackageDir(dir) createBinaryPackage(dir) - // Create a stale .opencode file - const cachedBinary = path.join(dir, "bin", ".opencode") + // Create a stale .altimate-code file + const cachedBinary = path.join(dir, "bin", ".altimate-code") fs.writeFileSync(cachedBinary, "stale content") const result = runPostinstall(dir) @@ -65,7 +65,7 @@ describe("postinstall.mjs", () => { // The current postinstall does not create bin/ — linkSync/copyFileSync fail // This test documents current behavior: it fails when bin/ is missing if (result.exitCode === 0) { - expect(fs.existsSync(path.join(dir, "bin", ".opencode"))).toBe(true) + expect(fs.existsSync(path.join(dir, "bin", ".altimate-code"))).toBe(true) } else { expect(result.exitCode).toBe(1) expect(result.stderr).toContain("Failed to setup altimate-code binary") diff --git a/packages/opencode/test/install/publish-package.test.ts b/packages/opencode/test/install/publish-package.test.ts index af0d79a5dc..625b8aa557 100644 --- a/packages/opencode/test/install/publish-package.test.ts +++ b/packages/opencode/test/install/publish-package.test.ts @@ -12,7 +12,7 @@ describe("publish package validation", () => { test("optionalDependencies has all expected platform packages", () => { // Validate that all expected platforms produce valid package names for (const p of EXPECTED_PLATFORMS) { - const pkgName = `@opencode-ai/opencode-${p}` + const pkgName = `@altimateai/altimate-code-${p}` expect(pkgName).toMatch(PACKAGE_NAME_PATTERN) } }) @@ -24,7 +24,7 @@ describe("publish package validation", () => { const version = "1.0.0" const binaries: Record = {} for (const p of EXPECTED_PLATFORMS) { - binaries[`@opencode-ai/opencode-${p}`] = version + binaries[`@altimateai/altimate-code-${p}`] = version } for (const [, ver] of Object.entries(binaries)) { expect(ver).not.toMatch(/^v/) @@ -34,7 +34,7 @@ describe("publish package validation", () => { test("package names follow naming convention", () => { for (const p of EXPECTED_PLATFORMS) { - const pkgName = `@opencode-ai/opencode-${p}` + const pkgName = `@altimateai/altimate-code-${p}` expect(pkgName).toMatch(PACKAGE_NAME_PATTERN) } }) @@ -43,7 +43,7 @@ describe("publish package validation", () => { const pkg = JSON.parse(fs.readFileSync(path.join(REPO_PKG_DIR, "package.json"), "utf-8")) expect(pkg.bin).toBeDefined() expect(pkg.bin["altimate"]).toBe("./bin/altimate") - expect(pkg.bin["altimate-code"]).toBe("./bin/altimate-code") + expect(pkg.bin["altimate-code"]).toBe("./bin/altimate") }) test("postinstall script has bun-then-node fallback", () => { @@ -66,10 +66,10 @@ describe("publish package validation", () => { // postinstall.mjs uses createRequire for resolving platform packages const postinstall = fs.readFileSync(path.join(REPO_PKG_DIR, "script/postinstall.mjs"), "utf-8") expect(postinstall).toContain("createRequire") - expect(postinstall).toContain("@opencode-ai/opencode-") + expect(postinstall).toContain("@altimateai/altimate-code-") // bin wrapper uses @altimateai scope - const wrapper = fs.readFileSync(path.join(REPO_PKG_DIR, "bin/altimate-code"), "utf-8") + const wrapper = fs.readFileSync(path.join(REPO_PKG_DIR, "bin/altimate"), "utf-8") expect(wrapper).toContain('"@altimateai"') }) }) diff --git a/packages/opencode/test/session/compaction-loop.test.ts b/packages/opencode/test/session/compaction-loop.test.ts index 208bd667e1..d4722e6776 100644 --- a/packages/opencode/test/session/compaction-loop.test.ts +++ b/packages/opencode/test/session/compaction-loop.test.ts @@ -496,7 +496,7 @@ describe("session.compaction.isOverflow boundary conditions", () => { await using tmp = await tmpdir({ init: async (dir) => { await Bun.write( - `${dir}/altimate-code.json`, + `${dir}/opencode.json`, JSON.stringify({ compaction: { reserved: 50_000 } }), ) }, @@ -517,7 +517,7 @@ describe("session.compaction.isOverflow boundary conditions", () => { await using tmp = await tmpdir({ init: async (dir) => { await Bun.write( - `${dir}/altimate-code.json`, + `${dir}/opencode.json`, JSON.stringify({ compaction: { reserved: 50_000 } }), ) }, @@ -565,7 +565,7 @@ describe("session.compaction.isOverflow boundary conditions", () => { await using tmp = await tmpdir({ init: async (dir) => { await Bun.write( - `${dir}/altimate-code.json`, + `${dir}/opencode.json`, JSON.stringify({ compaction: { prune: false } }), ) }, @@ -587,7 +587,7 @@ describe("session.compaction.prune with disabled config", () => { await using tmp = await tmpdir({ init: async (dir) => { await Bun.write( - `${dir}/altimate-code.json`, + `${dir}/opencode.json`, JSON.stringify({ compaction: { prune: false } }), ) }, diff --git a/packages/opencode/test/session/compaction.test.ts b/packages/opencode/test/session/compaction.test.ts index 452926d12e..03671bde51 100644 --- a/packages/opencode/test/session/compaction.test.ts +++ b/packages/opencode/test/session/compaction.test.ts @@ -228,14 +228,14 @@ describe("session.compaction.isOverflow", () => { }) describe("util.token.estimate", () => { - test("estimates tokens from text (4 chars per token)", () => { + test("estimates tokens from text (3.7 chars per token default)", () => { const text = "x".repeat(4000) - expect(Token.estimate(text)).toBe(1000) + expect(Token.estimate(text)).toBe(Math.round(4000 / 3.7)) }) test("estimates tokens from larger text", () => { const text = "y".repeat(20_000) - expect(Token.estimate(text)).toBe(5000) + expect(Token.estimate(text)).toBe(Math.round(20_000 / 3.7)) }) test("returns 0 for empty string", () => { diff --git a/packages/opencode/test/tool/project-scan.test.ts b/packages/opencode/test/tool/project-scan.test.ts index fd2fd92efc..5210d52f49 100644 --- a/packages/opencode/test/tool/project-scan.test.ts +++ b/packages/opencode/test/tool/project-scan.test.ts @@ -17,7 +17,7 @@ import { type EnvVarConnection, type DataToolInfo, type ConfigFileInfo, -} from "../../src/tool/project-scan" +} from "../../src/altimate/tools/project-scan" // --------------------------------------------------------------------------- // Helpers diff --git a/packages/opencode/test/tool/read.test.ts b/packages/opencode/test/tool/read.test.ts index b22fc3e712..2633b56048 100644 --- a/packages/opencode/test/tool/read.test.ts +++ b/packages/opencode/test/tool/read.test.ts @@ -13,7 +13,7 @@ const ctx = { sessionID: "test", messageID: "", callID: "", - agent: "build", + agent: "builder", abort: AbortSignal.any([]), messages: [], metadata: () => {}, @@ -163,7 +163,7 @@ describe("tool.read env file permissions", () => { ["environment.ts", false], ] - describe.each(["build", "plan"])("agent=%s", (agentName) => { + describe.each(["builder", "plan"])("agent=%s", (agentName) => { test.each(cases)("%s asks=%s", async (filename, shouldAsk) => { await using tmp = await tmpdir({ init: (dir) => Bun.write(path.join(dir, filename), "content"), diff --git a/packages/util/src/error.ts b/packages/util/src/error.ts index 12c27a0a77..5da4f9dae8 100644 --- a/packages/util/src/error.ts +++ b/packages/util/src/error.ts @@ -27,7 +27,7 @@ export abstract class NamedError extends Error { } static isInstance(input: any): input is InstanceType { - return typeof input === "object" && "name" in input && input.name === name + return input !== null && typeof input === "object" && "name" in input && input.name === name } schema() {