feat(aircoding): AirCoding V2 baseline — deterministic multi-agent architecture
Forked from OpenCode v1.17.4 with multi-agent system: - 5 agents: aircoding, scheduler, worker, architect, reviewer - Deterministic DAG scheduling engine (coordinator_tick) - Tool whitelists as hard enforcement - AirCoding validation plugin - V1 requirements: C4 docs, ADR, AGENTS.md, debug-log.md - Design documents in docs/
This commit is contained in:
33
packages/tui/test/prompt/display.test.ts
Normal file
33
packages/tui/test/prompt/display.test.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import { displayCharAt, displaySlice, mentionTriggerIndex } from "../../src/prompt/display"
|
||||
|
||||
describe("prompt display", () => {
|
||||
test("uses display-width offsets for mentions", () => {
|
||||
expect(mentionTriggerIndex("@")).toBe(0)
|
||||
expect(mentionTriggerIndex("test @")).toBe(5)
|
||||
expect(mentionTriggerIndex("中文 @")).toBe(5)
|
||||
expect(mentionTriggerIndex("こんにちは @")).toBe(11)
|
||||
expect(mentionTriggerIndex("한국어 @")).toBe(7)
|
||||
expect(mentionTriggerIndex("🙂 @")).toBe(3)
|
||||
expect(mentionTriggerIndex("中文 @src file", Bun.stringWidth("中文 @src"))).toBe(5)
|
||||
expect(displayCharAt("中文 @src", Bun.stringWidth("中文 @"))).toBe("s")
|
||||
expect(displaySlice("中文 @src", 5, Bun.stringWidth("中文 @src"))).toBe("@src")
|
||||
expect(displaySlice("中文 @src", 6, Bun.stringWidth("中文 @src"))).toBe("src")
|
||||
expect(mentionTriggerIndex("👨👩👧👦 @src", Bun.stringWidth("👨👩👧👦 @src"))).toBe(3)
|
||||
expect(displayCharAt("👨👩👧👦 @src", Bun.stringWidth("👨👩👧👦 @"))).toBe("s")
|
||||
expect(displaySlice("👨👩👧👦 @src", 3, Bun.stringWidth("👨👩👧👦 @src"))).toBe("@src")
|
||||
expect(mentionTriggerIndex("@file1\n@file2", 13)).toBe(7)
|
||||
expect(displayCharAt("@file1\n@file2", 6)).toBe("\n")
|
||||
expect(displaySlice("@file1\n@file2", 8, 13)).toBe("file2")
|
||||
expect(mentionTriggerIndex("@file1\nfoo @file2", 17)).toBe(11)
|
||||
expect(mentionTriggerIndex("中文 @one\n@two", 14)).toBe(10)
|
||||
expect(displaySlice("中文 @one\n@two", 11, 14)).toBe("two")
|
||||
expect(mentionTriggerIndex("中文@")).toBeUndefined()
|
||||
expect(mentionTriggerIndex("こんにちは@")).toBeUndefined()
|
||||
expect(mentionTriggerIndex("한국어@")).toBeUndefined()
|
||||
expect(mentionTriggerIndex("🙂@")).toBeUndefined()
|
||||
expect(mentionTriggerIndex("hello@")).toBeUndefined()
|
||||
expect(mentionTriggerIndex("foo@bar.com")).toBeUndefined()
|
||||
expect(mentionTriggerIndex("中文 @src file")).toBeUndefined()
|
||||
})
|
||||
})
|
||||
39
packages/tui/test/prompt/history.test.ts
Normal file
39
packages/tui/test/prompt/history.test.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import { isDuplicateEntry, MAX_HISTORY_ENTRIES, parsePromptHistory, type PromptInfo } from "../../src/prompt/history"
|
||||
|
||||
const entry = (input: string, parts: PromptInfo["parts"] = []): PromptInfo => ({ input, parts })
|
||||
|
||||
describe("prompt history", () => {
|
||||
test("recovers valid JSONL entries around corruption", () => {
|
||||
expect(parsePromptHistory(`${JSON.stringify(entry("one"))}\nnot-json\n${JSON.stringify(entry("two"))}\n`)).toEqual([
|
||||
entry("one"),
|
||||
entry("two"),
|
||||
])
|
||||
})
|
||||
|
||||
test("retains only the newest entries", () => {
|
||||
const input = Array.from({ length: MAX_HISTORY_ENTRIES + 5 }, (_, index) =>
|
||||
JSON.stringify(entry(String(index))),
|
||||
).join("\n")
|
||||
const result = parsePromptHistory(input)
|
||||
expect(result).toHaveLength(MAX_HISTORY_ENTRIES)
|
||||
expect(result[0]?.input).toBe("5")
|
||||
})
|
||||
|
||||
test("dedupes only identical consecutive entries", () => {
|
||||
expect(isDuplicateEntry(undefined, entry("hello"))).toBe(false)
|
||||
expect(isDuplicateEntry(entry("hello"), entry("hello"))).toBe(true)
|
||||
expect(isDuplicateEntry(entry("foo"), entry("bar"))).toBe(false)
|
||||
expect(isDuplicateEntry({ ...entry("ls"), mode: "normal" }, { ...entry("ls"), mode: "shell" })).toBe(false)
|
||||
})
|
||||
|
||||
test("does not dedupe entries with different parts", () => {
|
||||
const a = entry("describe this", [
|
||||
{ type: "file", mime: "image/png", filename: "a.png", url: "data:image/png;base64,AAA" },
|
||||
])
|
||||
const b = entry("describe this", [
|
||||
{ type: "file", mime: "image/png", filename: "b.png", url: "data:image/png;base64,BBB" },
|
||||
])
|
||||
expect(isDuplicateEntry(a, b)).toBe(false)
|
||||
})
|
||||
})
|
||||
24
packages/tui/test/prompt/jsonl.test.ts
Normal file
24
packages/tui/test/prompt/jsonl.test.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import { expect, test } from "bun:test"
|
||||
import { MAX_FRECENCY_ENTRIES, parseFrecency } from "../../src/prompt/frecency"
|
||||
import { MAX_STASH_ENTRIES, parsePromptStash } from "../../src/prompt/stash"
|
||||
|
||||
test("stash JSONL skips corruption and retains newest entries", () => {
|
||||
const entries = Array.from({ length: MAX_STASH_ENTRIES + 2 }, (_, index) =>
|
||||
JSON.stringify({ input: String(index), parts: [], timestamp: index }),
|
||||
)
|
||||
entries.splice(2, 0, "broken")
|
||||
const result = parsePromptStash(entries.join("\n"))
|
||||
expect(result).toHaveLength(MAX_STASH_ENTRIES)
|
||||
expect(result[0]?.input).toBe("2")
|
||||
})
|
||||
|
||||
test("frecency JSONL skips corruption, keeps latest path state, and limits entries", () => {
|
||||
const entries = Array.from({ length: MAX_FRECENCY_ENTRIES + 1 }, (_, index) =>
|
||||
JSON.stringify({ path: String(index), frequency: 1, lastOpen: index }),
|
||||
)
|
||||
entries.push("broken", JSON.stringify({ path: "1000", frequency: 2, lastOpen: 2000 }))
|
||||
const result = parseFrecency(entries.join("\n"))
|
||||
expect(result).toHaveLength(MAX_FRECENCY_ENTRIES)
|
||||
expect(result[0]).toEqual({ path: "1000", frequency: 2, lastOpen: 2000 })
|
||||
expect(result.some((entry) => entry.path === "0")).toBe(false)
|
||||
})
|
||||
43
packages/tui/test/prompt/local-attachment.test.ts
Normal file
43
packages/tui/test/prompt/local-attachment.test.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import { readLocalAttachmentWith } from "../../src/component/prompt/local-attachment"
|
||||
import type { LocalFiles } from "../../src/component/prompt/local-attachment"
|
||||
|
||||
function files(input: { mime: string; text?: string; bytes?: Uint8Array }): LocalFiles {
|
||||
return {
|
||||
mime: async () => input.mime,
|
||||
readText: async () => input.text ?? "",
|
||||
readBytes: async () => input.bytes ?? new Uint8Array(),
|
||||
}
|
||||
}
|
||||
|
||||
describe("prompt local attachments", () => {
|
||||
test("reads SVG attachments as text", async () => {
|
||||
expect(await readLocalAttachmentWith(files({ mime: "image/svg+xml", text: "<svg />" }), "/tmp/image.svg")).toEqual({
|
||||
type: "text",
|
||||
mime: "image/svg+xml",
|
||||
content: "<svg />",
|
||||
})
|
||||
})
|
||||
|
||||
test("reads image and PDF attachments as bytes", async () => {
|
||||
const content = new Uint8Array([1, 2, 3])
|
||||
expect(await readLocalAttachmentWith(files({ mime: "application/pdf", bytes: content }), "/tmp/file.pdf")).toEqual({
|
||||
type: "binary",
|
||||
mime: "application/pdf",
|
||||
content,
|
||||
})
|
||||
})
|
||||
|
||||
test("ignores unsupported and unreadable local files", async () => {
|
||||
expect(await readLocalAttachmentWith(files({ mime: "text/plain" }), "/tmp/file.txt")).toBeUndefined()
|
||||
expect(
|
||||
await readLocalAttachmentWith(
|
||||
{
|
||||
...files({ mime: "image/png" }),
|
||||
readBytes: async () => Promise.reject(new Error("missing")),
|
||||
},
|
||||
"/tmp/missing.png",
|
||||
),
|
||||
).toBeUndefined()
|
||||
})
|
||||
})
|
||||
53
packages/tui/test/prompt/part.test.ts
Normal file
53
packages/tui/test/prompt/part.test.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import { expandTrackedPastedText, stripPromptPartIDs } from "../../src/prompt/part"
|
||||
|
||||
describe("prompt part", () => {
|
||||
test("strips persisted IDs from reused parts", () => {
|
||||
expect(
|
||||
stripPromptPartIDs({
|
||||
id: "prt_old",
|
||||
sessionID: "ses_old",
|
||||
messageID: "msg_old",
|
||||
type: "file" as const,
|
||||
mime: "image/png",
|
||||
filename: "tiny.png",
|
||||
url: "data:image/png;base64,abc",
|
||||
}),
|
||||
).toEqual({
|
||||
type: "file",
|
||||
mime: "image/png",
|
||||
filename: "tiny.png",
|
||||
url: "data:image/png;base64,abc",
|
||||
})
|
||||
})
|
||||
|
||||
test("preserves wide characters around pasted text", () => {
|
||||
const marker = "[Pasted ~3 lines]"
|
||||
const prefix = "你好你好\n"
|
||||
|
||||
expect(
|
||||
expandTrackedPastedText(prefix + marker + "\n阿斯顿法国红酒看来", [
|
||||
{
|
||||
start: Bun.stringWidth("你好你好") + 1,
|
||||
end: Bun.stringWidth("你好你好") + 1 + Bun.stringWidth(marker),
|
||||
text: "public:\n\tvoid ExecuteTask();\nprivate:",
|
||||
},
|
||||
]),
|
||||
).toBe("你好你好\npublic:\n\tvoid ExecuteTask();\nprivate:\n阿斯顿法国红酒看来")
|
||||
})
|
||||
|
||||
test("only expands the tracked placeholder occurrence", () => {
|
||||
const marker = "[Pasted ~3 lines]"
|
||||
const prefix = `keep ${marker} then `
|
||||
|
||||
expect(
|
||||
expandTrackedPastedText(prefix + marker + " tail", [
|
||||
{
|
||||
start: Bun.stringWidth(prefix),
|
||||
end: Bun.stringWidth(prefix + marker),
|
||||
text: "alpha\nbeta\ngamma",
|
||||
},
|
||||
]),
|
||||
).toBe(`keep ${marker} then alpha\nbeta\ngamma tail`)
|
||||
})
|
||||
})
|
||||
23
packages/tui/test/prompt/persistence.test.ts
Normal file
23
packages/tui/test/prompt/persistence.test.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { expect, test } from "bun:test"
|
||||
import path from "path"
|
||||
import { mkdtemp, rm } from "fs/promises"
|
||||
import { tmpdir } from "os"
|
||||
import { appendText, readJson, readText, writeJsonAtomic, writeText } from "../../src/util/persistence"
|
||||
|
||||
test("persistence creates parent directories and supports text, append, and JSON", async () => {
|
||||
const root = await mkdtemp(path.join(tmpdir(), "opencode-tui-persistence-"))
|
||||
try {
|
||||
const textPath = path.join(root, "nested", "state.jsonl")
|
||||
await writeText(textPath, "one\n")
|
||||
await appendText(textPath, "two\n")
|
||||
expect(await readText(textPath)).toBe("one\ntwo\n")
|
||||
|
||||
const jsonPath = path.join(root, "other", "state.json")
|
||||
await writeJsonAtomic(jsonPath, { value: 1 })
|
||||
expect(await readJson<{ value: number }>(jsonPath)).toEqual({ value: 1 })
|
||||
await writeJsonAtomic(jsonPath, { value: 2 })
|
||||
expect(await readJson<{ value: number }>(jsonPath)).toEqual({ value: 2 })
|
||||
} finally {
|
||||
await rm(root, { recursive: true, force: true })
|
||||
}
|
||||
})
|
||||
25
packages/tui/test/prompt/traits.test.ts
Normal file
25
packages/tui/test/prompt/traits.test.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import { computePromptTraits } from "../../src/prompt/traits"
|
||||
|
||||
describe("computePromptTraits", () => {
|
||||
test("normal mode without autocomplete only captures tab", () => {
|
||||
const traits = computePromptTraits({ mode: "normal", autocompleteVisible: false })
|
||||
expect(traits.capture).toEqual(["tab"])
|
||||
expect(traits.suspend).toBeUndefined()
|
||||
expect(traits.status).toBeUndefined()
|
||||
})
|
||||
|
||||
test("normal mode with autocomplete captures navigation keys", () => {
|
||||
const traits = computePromptTraits({ mode: "normal", autocompleteVisible: true })
|
||||
expect(traits.capture).toEqual(["escape", "navigate", "submit", "tab"])
|
||||
expect(traits.suspend).toBeUndefined()
|
||||
expect(traits.status).toBeUndefined()
|
||||
})
|
||||
|
||||
test("shell mode disables capture and labels the prompt without suspending", () => {
|
||||
const traits = computePromptTraits({ mode: "shell", autocompleteVisible: false })
|
||||
expect(traits.capture).toBeUndefined()
|
||||
expect(traits.suspend).toBeUndefined()
|
||||
expect(traits.status).toBe("SHELL")
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user