{"version":3,"file":"interactive-mode-operator-stack.test.d.ts","sourceRoot":"","sources":["../../../src/modes/interactive/interactive-mode-operator-stack.test.ts"],"names":[],"mappings":"","sourcesContent":["import { Container } from \"@apholdings/jensen-tui\";\nimport stripAnsi from \"strip-ansi\";\nimport { beforeAll, describe, expect, it, vi } from \"vitest\";\nimport type { AgentSessionEvent } from \"../../core/agent-session.js\";\nimport type { Task } from \"../../core/memory.js\";\nimport type { WorkingContext } from \"../../core/working-context.js\";\nimport { SidebarTodoPanel } from \"./components/sidebar-todo-panel.js\";\nimport { InteractiveMode, mountOperatorStack } from \"./interactive-mode.js\";\nimport { initTheme } from \"./theme/theme.js\";\n\n/**\n * End-to-end proof for the Working Context → Todos → Tasks operator stack.\n *\n * These tests exercise the real seam between session events and the mounted\n * panels: they drive InteractiveMode.prototype.handleEvent with real\n * `todo_update` / `task_update` events, let it flow through\n * `updateWorkingContextPanel` → `updateSidebarTodoPanel` /\n * `updateSidebarTaskPanel`, and assert against the rendered output of the\n * panels mounted in the stacked UI container in their real order.\n *\n * The InteractiveMode constructor is intentionally bypassed (it requires a\n * live ProcessTerminal and a full session bootstrap). We construct a partial\n * instance via `Object.create(InteractiveMode.prototype)`, attach real panel\n * instances and a real Container that stands in for `this.ui`, and wire a\n * stateful mock session that mutates its todo/task arrays between events.\n */\n\ninterface OperatorStackHarness {\n\tsession: StatefulMockSession;\n\tui: Container & { requestRender: () => void };\n\tsidebarTodoPanel: SidebarTodoPanel;\n\tsidebarTaskPanel: SidebarTodoPanel;\n\tfooter: { invalidate: () => void };\n\thandleEvent: (event: AgentSessionEvent) => Promise<void>;\n}\n\ninterface StatefulMockSession {\n\t_todos: Array<{ content: string; activeForm: string; status: \"pending\" | \"in_progress\" | \"completed\" }>;\n\t_tasks: Task[];\n\tgetTodos: () => StatefulMockSession[\"_todos\"];\n\tgetTasks: () => Task[];\n\tgetWorkingContext: () => WorkingContext;\n}\n\nbeforeAll(() => {\n\tinitTheme(\"dark\");\n});\n\nfunction createStatefulMockSession(): StatefulMockSession {\n\tconst todos: StatefulMockSession[\"_todos\"] = [];\n\tconst tasks: Task[] = [];\n\n\tconst session: StatefulMockSession = {\n\t\t_todos: todos,\n\t\t_tasks: tasks,\n\t\tgetTodos: () => todos,\n\t\tgetTasks: () => tasks,\n\t\tgetWorkingContext: (): WorkingContext =>\n\t\t\t({\n\t\t\t\tmemory: {\n\t\t\t\t\titemCount: 0,\n\t\t\t\t\tstaleCount: 0,\n\t\t\t\t\tkeyPreview: [],\n\t\t\t\t\tisPersisted: true,\n\t\t\t\t\tscope: \"current_branch_session_state\",\n\t\t\t\t},\n\t\t\t\ttodo: {\n\t\t\t\t\ttotal: todos.length,\n\t\t\t\t\tcompleted: todos.filter((t) => t.status === \"completed\").length,\n\t\t\t\t\tinProgress: todos.find((t) => t.status === \"in_progress\")?.activeForm,\n\t\t\t\t\tisPersisted: true,\n\t\t\t\t\tscope: \"current_branch_session_state\",\n\t\t\t\t},\n\t\t\t\ttasks: {\n\t\t\t\t\ttotal: tasks.length,\n\t\t\t\t\tpending: tasks.filter((t) => t.status === \"pending\").length,\n\t\t\t\t\tinProgress: tasks.filter((t) => t.status === \"in_progress\").length,\n\t\t\t\t\tcompleted: tasks.filter((t) => t.status === \"completed\").length,\n\t\t\t\t\tinProgressTask: tasks.find((t) => t.status === \"in_progress\")\n\t\t\t\t\t\t? {\n\t\t\t\t\t\t\t\tid: tasks.find((t) => t.status === \"in_progress\")!.id,\n\t\t\t\t\t\t\t\tsubject: tasks.find((t) => t.status === \"in_progress\")!.subject,\n\t\t\t\t\t\t\t\tactiveForm: tasks.find((t) => t.status === \"in_progress\")!.activeForm,\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t: undefined,\n\t\t\t\t\tisPersisted: true,\n\t\t\t\t\tscope: \"current_branch_session_state\",\n\t\t\t\t},\n\t\t\t\tdelegatedWork: {\n\t\t\t\t\tactiveCount: 0,\n\t\t\t\t\tcompletedCount: 0,\n\t\t\t\t\tfailedCount: 0,\n\t\t\t\t\tactiveAgents: [],\n\t\t\t\t\tfailurePreview: [],\n\t\t\t\t\tisPersisted: false,\n\t\t\t\t\tscope: \"current_process_runtime_state\",\n\t\t\t\t\tnote: \"live current-process state only; not persisted and resets on session switch/resume\",\n\t\t\t\t},\n\t\t\t}) as WorkingContext,\n\t} as unknown as StatefulMockSession;\n\n\treturn session;\n}\n\nfunction createHarness(): OperatorStackHarness {\n\tconst session = createStatefulMockSession();\n\tconst ui = new Container() as Container & { requestRender: () => void };\n\tui.requestRender = vi.fn();\n\n\tconst sidebarTodoPanel = new SidebarTodoPanel({ title: \"Todos\" });\n\tconst sidebarTaskPanel = new SidebarTodoPanel({ title: \"Tasks\" });\n\n\t// Use the same production assembly path as InteractiveMode.init()\n\tmountOperatorStack(ui, sidebarTodoPanel, sidebarTaskPanel);\n\n\tconst harness = Object.assign(Object.create(InteractiveMode.prototype), {\n\t\tsession,\n\t\tui,\n\t\tsidebarTodoPanel,\n\t\tsidebarTaskPanel,\n\t\tfooter: { invalidate: vi.fn() },\n\t\tisInitialized: true,\n\t\tsessionEpoch: 1,\n\t}) as OperatorStackHarness;\n\n\treturn harness;\n}\n\nfunction renderStack(harness: OperatorStackHarness, width = 100): string {\n\treturn stripAnsi(harness.ui.render(width).join(\"\\n\"));\n}\n\ndescribe(\"InteractiveMode operator stack (Working Context / Todos / Tasks)\", () => {\n\tit(\"renders nothing from todo/task panels in empty state\", async () => {\n\t\tconst harness = createHarness();\n\n\t\tawait harness.handleEvent({ type: \"todo_update\", todos: [] });\n\t\tawait harness.handleEvent({ type: \"task_update\", tasks: [] });\n\n\t\tconst todoLines = harness.sidebarTodoPanel.render(100);\n\t\tconst taskLines = harness.sidebarTaskPanel.render(100);\n\t\texpect(todoLines).toEqual([]);\n\t\texpect(taskLines).toEqual([]);\n\n\t\tconst stack = renderStack(harness);\n\t\texpect(stack).not.toContain(\"Todos\");\n\t\texpect(stack).not.toContain(\"Tasks \");\n\t});\n\n\tit(\"renders the todo panel live after a real todo_update event\", async () => {\n\t\tconst harness = createHarness();\n\n\t\tharness.session._todos.push(\n\t\t\t{ content: \"First task\", activeForm: \"Working on first task\", status: \"in_progress\" },\n\t\t\t{ content: \"Second task\", activeForm: \"Working on second task\", status: \"pending\" },\n\t\t\t{ content: \"Third task\", activeForm: \"Working on third task\", status: \"pending\" },\n\t\t);\n\t\tawait harness.handleEvent({ type: \"todo_update\", todos: harness.session._todos });\n\n\t\tconst stack = renderStack(harness);\n\t\texpect(stack).toContain(\"Todos\");\n\t\texpect(stack).toContain(\"0/3\");\n\t\texpect(stack).toContain(\"Working on first task\");\n\t\texpect(stack).toContain(\"Second task\");\n\t\texpect(stack).toContain(\"Third task\");\n\t\t// ui.requestRender was called by the handler\n\t\texpect(harness.ui.requestRender).toHaveBeenCalled();\n\t});\n\n\tit(\"renders the task panel live after a real task_update event\", async () => {\n\t\tconst harness = createHarness();\n\n\t\tharness.session._tasks.push(\n\t\t\t{\n\t\t\t\tid: \"task_1\",\n\t\t\t\tsubject: \"Ship the feature\",\n\t\t\t\tdescription: \"\",\n\t\t\t\tstatus: \"in_progress\",\n\t\t\t\tactiveForm: \"Shipping the feature\",\n\t\t\t},\n\t\t\t{ id: \"task_2\", subject: \"Write docs\", description: \"\", status: \"pending\" },\n\t\t);\n\t\tawait harness.handleEvent({ type: \"task_update\", tasks: harness.session._tasks });\n\n\t\tconst stack = renderStack(harness);\n\t\texpect(stack).toContain(\"Tasks\");\n\t\texpect(stack).toContain(\"0/2\");\n\t\texpect(stack).toContain(\"Shipping the feature\");\n\t\texpect(stack).toContain(\"Write docs\");\n\t});\n\n\tit(\"keeps Working Context → Todos → Tasks order in the mounted stack\", async () => {\n\t\tconst harness = createHarness();\n\n\t\tharness.session._todos.push({\n\t\t\tcontent: \"Do the thing\",\n\t\t\tactiveForm: \"Doing the thing\",\n\t\t\tstatus: \"in_progress\",\n\t\t});\n\t\tharness.session._tasks.push({\n\t\t\tid: \"task_1\",\n\t\t\tsubject: \"Ship it\",\n\t\t\tdescription: \"\",\n\t\t\tstatus: \"in_progress\",\n\t\t\tactiveForm: \"Shipping it\",\n\t\t});\n\t\tawait harness.handleEvent({ type: \"todo_update\", todos: harness.session._todos });\n\t\tawait harness.handleEvent({ type: \"task_update\", tasks: harness.session._tasks });\n\n\t\tconst stack = renderStack(harness);\n\t\tconst todosIdx = stack.indexOf(\"Todos\");\n\t\tconst tasksIdx = stack.indexOf(\"Tasks \");\n\n\t\texpect(todosIdx).toBeGreaterThanOrEqual(0);\n\t\texpect(tasksIdx).toBeGreaterThan(todosIdx);\n\t});\n\n\tit(\"reflects live updates: adding, then flipping a task to completed\", async () => {\n\t\tconst harness = createHarness();\n\n\t\tharness.session._tasks.push({\n\t\t\tid: \"task_1\",\n\t\t\tsubject: \"Review PR\",\n\t\t\tdescription: \"\",\n\t\t\tstatus: \"in_progress\",\n\t\t\tactiveForm: \"Reviewing PR\",\n\t\t});\n\t\tawait harness.handleEvent({ type: \"task_update\", tasks: harness.session._tasks });\n\n\t\tlet stack = renderStack(harness);\n\t\texpect(stack).toContain(\"Reviewing PR\");\n\t\texpect(stack).toContain(\"0/1\");\n\n\t\t// Flip to completed in place; same event wakes the same mounted panel\n\t\tharness.session._tasks[0]!.status = \"completed\";\n\t\tawait harness.handleEvent({ type: \"task_update\", tasks: harness.session._tasks });\n\n\t\tstack = renderStack(harness);\n\t\texpect(stack).toContain(\"1/1\");\n\t\t// Completed item renders content (not activeForm)\n\t\texpect(stack).toContain(\"Review PR\");\n\t});\n\n\tit(\"does not duplicate mounted panels across repeated events\", async () => {\n\t\tconst harness = createHarness();\n\t\tconst initialChildrenLength = harness.ui.children.length;\n\n\t\tfor (let i = 0; i < 5; i++) {\n\t\t\tawait harness.handleEvent({ type: \"todo_update\", todos: [] });\n\t\t\tawait harness.handleEvent({ type: \"task_update\", tasks: [] });\n\t\t}\n\n\t\texpect(harness.ui.children.length).toBe(initialChildrenLength);\n\t\texpect(harness.ui.children).toContain(harness.sidebarTodoPanel);\n\t\texpect(harness.ui.children).toContain(harness.sidebarTaskPanel);\n\t});\n\n\tit(\"transitions from visible back to empty-state cleanly when items are cleared\", async () => {\n\t\tconst harness = createHarness();\n\n\t\tharness.session._todos.push({ content: \"A\", activeForm: \"Doing A\", status: \"in_progress\" });\n\t\tawait harness.handleEvent({ type: \"todo_update\", todos: harness.session._todos });\n\t\texpect(renderStack(harness)).toContain(\"Todos\");\n\n\t\t// Clear in place\n\t\tharness.session._todos.length = 0;\n\t\tawait harness.handleEvent({ type: \"todo_update\", todos: [] });\n\n\t\texpect(harness.sidebarTodoPanel.render(100)).toEqual([]);\n\t\t// Task panel still empty too; stack collapses cleanly\n\t\texpect(harness.sidebarTaskPanel.render(100)).toEqual([]);\n\t\t// Container still has both panels mounted (no remounting)\n\t\texpect(harness.ui.children).toHaveLength(2);\n\t});\n});\n\ndescribe(\"mountOperatorStack (production assembly path)\", () => {\n\tit(\"mounts panels in Todos → Tasks order\", () => {\n\t\tconst container = new Container();\n\n\t\tconst sidebarTodoPanel = new SidebarTodoPanel({ title: \"Todos\" });\n\t\tconst sidebarTaskPanel = new SidebarTodoPanel({ title: \"Tasks\" });\n\n\t\tmountOperatorStack(container, sidebarTodoPanel, sidebarTaskPanel);\n\n\t\texpect(container.children).toHaveLength(2);\n\t\texpect(container.children[0]).toBe(sidebarTodoPanel);\n\t\texpect(container.children[1]).toBe(sidebarTaskPanel);\n\t});\n\n\tit(\"is idempotent: calling multiple times does not duplicate panels\", () => {\n\t\tconst container = new Container();\n\n\t\tconst sidebarTodoPanel = new SidebarTodoPanel({ title: \"Todos\" });\n\t\tconst sidebarTaskPanel = new SidebarTodoPanel({ title: \"Tasks\" });\n\n\t\tmountOperatorStack(container, sidebarTodoPanel, sidebarTaskPanel);\n\t\tmountOperatorStack(container, sidebarTodoPanel, sidebarTaskPanel);\n\t\tmountOperatorStack(container, sidebarTodoPanel, sidebarTaskPanel);\n\n\t\texpect(container.children).toHaveLength(2);\n\t});\n\n\tit(\"is the same path InteractiveMode.init() calls (harness uses the exported helper)\", () => {\n\t\t// createHarness() calls mountOperatorStack to build the container — this test\n\t\t// confirms that the harness and production code share the same assembly function.\n\t\tconst harness = createHarness();\n\n\t\texpect(harness.ui.children[0]).toBe(harness.sidebarTodoPanel);\n\t\texpect(harness.ui.children[1]).toBe(harness.sidebarTaskPanel);\n\t});\n});\n"]}