Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 7 additions & 3 deletions packages/app-bundle/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -919,6 +919,10 @@
"packages/app/public/assets/JuliaMono-Regular.woff2": "43f5dfca02a03035f89799da5af150f28d5c4aff729753076094c687fc61b063",
"packages/app/public/assets/RacingSansOne-Regular.woff2": "2ff92c8abe7a35962922c54da215d68690f8502463698b1b595bed597b63e3ff",
"packages/app/public/oc-theme-preload.js": "27227e802b3494e7c545da903e679efdb30ccc754cd4eb5cdf08005a40d560b6",
"packages/app/src/amicode/campaign/campaign-tab-model.ts": "4841be5aa39a36959e94216a052296a7ffdbd2ae4479149f36d8aaef2ee573cb",
"packages/app/src/amicode/campaign/campaign-tab.tsx": "7da4420dbabb34ad6e1b116b193b1daf3eb44458a7f14ff3f330f89a261921fa",
"packages/app/src/amicode/campaign/campaign-tab.test.ts": "b4ba7897615708bc17acacedc62db3ac29290b475db363deae64c31d166a310d",
"packages/app/src/amicode/campaign/campaign-tab-model.test.ts": "c6037af793de92b365564fbe4f2413a74dd350a4102bc795d36f4d73ccede541",
"packages/app/src/amicode/inspector/device-inspector.tsx": "5ab63709e88176bb4dad1072ffe28fd59ad67d414c9d844ee6c55353df50df79",
"packages/app/src/amicode/inspector/inspector-bridge.ts": "460dbf4509b4e56a8eacd6cac7adcb3a63c6cd3e417d851df5f3e3b259095d3e",
"packages/app/src/amicode/inspector/inspector-context.tsx": "ac81b9e0e2a4035f85b6af68b7f0cefcb9f256a175b244ba13136f3a307c0c77",
Expand Down Expand Up @@ -1097,14 +1101,14 @@
"packages/app/src/pages/session/composer/session-tour.test.ts": "147473505d54466417b6b34fa672d280b71e1b9577e8bd41fbf9870035d1083e",
"packages/app/src/pages/session/composer/session-tour.tsx": "da5d807c7efe7aed1140d65d7eddd7982a8c17348d928f5d0a887d1b2a8d3a50",
"packages/app/src/pages/session/composer/todo-panel-motion.stories.tsx": "70a8267c725af320d78d266b85fd0ff005d067c4d09a21bef824126602e831e7",
"packages/app/src/pages/session/helpers.test.ts": "a473d86117e3fddd25ba28a189d90fa35bcb80da7f2a152d3188e139f375e45d",
"packages/app/src/pages/session/helpers.ts": "8d0106a5ec3f01a666bd840e20b6bfb28d0e88b8c8c51fc1fdd7eaa33e9daafc",
"packages/app/src/pages/session/helpers.test.ts": "77d8c3100793686f40b5982187783d20fa883aa7a5df1fe7c0f01970a750d9c1",
"packages/app/src/pages/session/helpers.ts": "fc0d700f4ada39dab274dca9dcc8ff784349c2ad5fb0314017a1e42408d827b1",
"packages/app/src/pages/session/session-panel-width.test.ts": "2b9daf379be0b54142dd791bed6bae6d215b2574176b3b3e4b1273db3baf433c",
"packages/app/src/pages/session/spawn-tabs.test.ts": "b0eaeb976f3ae520fe38cd6b4cc1493b06c626923efaa2f57c22c70a638fb511",
"packages/app/src/pages/session/spawn-tabs.ts": "e5ae126808831944199244f67e4475aeb8f31a21c9eeff70eb3cd1ec05f6c5da",
"packages/app/src/pages/session/session-panel-width.ts": "8723cb2f980972ea9bf182240fdf154d131bf40e22fe563fd538a7005cff9192",
"packages/app/src/pages/session/session-side-panel-structure.test.ts": "c138fe905498c8326f459dccba61b146af6dfb12b78480303723b5a85046931a",
"packages/app/src/pages/session/session-side-panel.tsx": "e2346a1f9d81c4f8436051ebf92dcd747be28f1e6b610d98792569067f28a4c0",
"packages/app/src/pages/session/session-side-panel.tsx": "6519eec611a7c2a67c33b2f6b5b0f25c06937f63979f2efc353f3a63de569e96",
"packages/app/src/pages/session/terminal-panel-v2.tsx": "68dad9307f1d2abf3ff9248e451bd08005acf01ddc46b30a6d01b58f4a90dce0",
"packages/app/src/pages/session/timeline/last-prompt-bubble.test.ts": "83753b3ec7227570523c2e0b9e82401fae7a20f3a23980bb40deaeb13304327b",
"packages/app/src/pages/session/timeline/message-timeline.tsx": "7db5ccaf96ad37fbc0fa078f65966f3b34a6102ce92e31e4c912ef89e5ecd0ea",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
// amicode#694: the Campaign tab's data path — pure projections from the
// campaign routes (#662) to the tab's render model, plus the deep-link
// recognizer for the digest tile's click path. The routes' wire shapes are
// snake_case (campaign_ledger.ts); the model stays mechanical: strip display
// markers, split table cells, never invent content.
import { describe, expect, test } from "bun:test"
import {
campaignLedgerModel,
campaignPromptSlug,
pickCampaign,
statusTone,
type CampaignDetailPayload,
type CampaignSummary,
} from "./campaign-tab-model"

const detail = (over: Partial<CampaignDetailPayload> = {}): CampaignDetailPayload => ({
slug: "session-20260830-spin-cz",
date: "2026-08-30",
campaign: "exchange-CZ",
status: "ACTIVE",
type: "autoresearch",
// §1's body — the route strips the `## 1.` header line (splitSections)
objective: "- Land the exchange-CZ campaign\n- stay under 200 ns",
verdicts: [
["Unit", "Scope", "Status"],
["H1 spec", "app overlay", "**DONE** — PR #17 merged"],
["calibration", "chip", "**BLOCKED** — awaiting chip time"],
],
active_work: "- refactoring the gate",
blocked: "- **chip time** — Shannon is booked until Thursday\n- probe rewire pending",
next_queue: "- sweep J coupling",
loop_log_tail: "| iter | verdict |\n| --- | --- |\n| 12 | spec landed |\n| 13 | blocked on chip |",
compaction: "(append-only: compaction log)",
sections_found: [1, 2, 3, 4, 5, 8],
file_date: "2026-08-30",
...over,
})

describe("campaignPromptSlug — the digest tile's click-path deep link", () => {
test("recognizes the tile's exact composition", () => {
expect(campaignPromptSlug("Open the campaign session-20260830-spin-cz")).toBe("session-20260830-spin-cz")
})

test("is case-insensitive on the phrase, exact on the slug", () => {
expect(campaignPromptSlug("open the campaign session-x")).toBe("session-x")
})

test("rejects non-campaign prompts and empty text", () => {
expect(campaignPromptSlug("Open the problem foo")).toBeNull()
expect(campaignPromptSlug("")).toBeNull()
expect(campaignPromptSlug("open the campaign")).toBeNull()
})

test("rejects slug-unsafe tails (traversal, spaces)", () => {
expect(campaignPromptSlug("Open the campaign ../etc/passwd")).toBeNull()
expect(campaignPromptSlug("Open the campaign two words")).toBeNull()
})
})

describe("pickCampaign — same rule as the digest tile (newest ACTIVE, newest fallback)", () => {
const entry = (slug: string, status: string | null): CampaignSummary => ({
slug,
date: null,
campaign: null,
status,
type: null,
objective: "",
})

test("prefers the newest ACTIVE entry; the list is newest-first", () => {
const list = [entry("a", "FINISHED"), entry("b", "ACTIVE")]
expect(pickCampaign(list)?.slug).toBe("b")
})

test("falls back to the newest overall when none is ACTIVE", () => {
const list = [entry("a", "FINISHED"), entry("b", null)]
expect(pickCampaign(list)?.slug).toBe("a")
})

test("empty list picks nothing", () => {
expect(pickCampaign([])).toBeUndefined()
})
})

describe("campaignLedgerModel — the full-ledger projection", () => {
test("maps the verdict table: header columns + unit/status/evidence rows", () => {
const model = campaignLedgerModel(detail())
expect(model.verdictColumns).toEqual(["Unit", "Scope", "Status"])
expect(model.verdicts).toEqual([
{ unit: "H1 spec", status: "DONE", evidence: "**DONE** — PR #17 merged" },
{ unit: "calibration", status: "BLOCKED", evidence: "**BLOCKED** — awaiting chip time" },
])
})

test("projects the status token mechanically (bold stripped, cut at the dash)", () => {
const model = campaignLedgerModel(detail())
expect(model.verdicts[0]?.status).toBe("DONE")
expect(model.verdicts[1]?.status).toBe("BLOCKED")
})

test("handles a verdict table without a header row", () => {
const model = campaignLedgerModel(detail({ verdicts: [["S1", "**DONE** — merged"]] }))
expect(model.verdictColumns).toEqual([])
expect(model.verdicts).toEqual([{ unit: "S1", status: "DONE", evidence: "**DONE** — merged" }])
})

test("renders the objective as marker-stripped lines", () => {
const model = campaignLedgerModel(detail())
expect(model.objectiveLines).toEqual(["Land the exchange-CZ campaign", "stay under 200 ns"])
})

test("renders the blocked queue with reasons as lines", () => {
const model = campaignLedgerModel(detail())
expect(model.blockedLines).toEqual(["chip time — Shannon is booked until Thursday", "probe rewire pending"])
})

test("keeps a table §8 loop-log tail as rows and a text tail as lines", () => {
const table = campaignLedgerModel(detail())
expect(table.loopLog).toEqual({ kind: "table", rows: [["iter", "verdict"], ["12", "spec landed"], ["13", "blocked on chip"]] })
const text = campaignLedgerModel(detail({ loop_log_tail: "loop 12 done\nloop 13 started" }))
expect(text.loopLog).toEqual({ kind: "text", lines: ["loop 12 done", "loop 13 started"] })
})

test("surfaces the frontmatter identity fields", () => {
const model = campaignLedgerModel(detail())
expect(model.slug).toBe("session-20260830-spin-cz")
expect(model.label).toBe("exchange-CZ")
expect(model.status).toBe("ACTIVE")
expect(model.date).toBe("2026-08-30")
})

test("falls back to the slug for the label and the filename date", () => {
const model = campaignLedgerModel(detail({ campaign: null, date: null }))
expect(model.label).toBe("session-20260830-spin-cz")
expect(model.date).toBe("2026-08-30") // file_date fallback
})

test("a null/undefined detail degrades to the empty model", () => {
const model = campaignLedgerModel(null)
expect(model.hasLedger).toBe(false)
expect(model.verdicts).toEqual([])
expect(model.objectiveLines).toEqual([])
})

test("a content-less ledger flags the empty state (the tab explains how one starts)", () => {
const model = campaignLedgerModel(
detail({ objective: "", verdicts: [], blocked: "", loop_log_tail: "" }),
)
expect(model.hasLedger).toBe(false)
})

test("an objective-less ledger with content still counts as a ledger", () => {
const model = campaignLedgerModel(detail({ objective: "" }))
expect(model.hasLedger).toBe(true)
})
})

describe("statusTone — the verdict chip tone discipline (theme tokens, no raw colors)", () => {
test("success / danger / neutral tones from the status token", () => {
expect(statusTone("DONE")).toBe("success")
expect(statusTone("MERGED")).toBe("success")
expect(statusTone("PASS")).toBe("success")
expect(statusTone("BLOCKED")).toBe("danger")
expect(statusTone("FAIL")).toBe("danger")
expect(statusTone("STUCK")).toBe("danger")
expect(statusTone("WIP")).toBe("neutral")
expect(statusTone("")).toBe("neutral")
})
})
Loading
Loading