@fiale-plus/pi-rogue-bundle 0.1.13 → 0.1.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,227 @@
1
+ import { describe, expect, it, vi, beforeEach, afterEach } from "vitest";
2
+ import { existsSync, readFileSync, writeFileSync, mkdirSync } from "node:fs";
3
+ import { join, dirname } from "node:path";
4
+ import { homedir } from "node:os";
5
+ import { registerAdvisor } from "./extension.js";
6
+
7
+ const testHome = vi.hoisted(() => `/tmp/pi-rogue-advisor-state-versioning-${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2)}`);
8
+
9
+ vi.mock("node:os", async () => {
10
+ const actual = await vi.importActual<typeof import("node:os")>("node:os");
11
+ return { ...actual, homedir: () => testHome };
12
+ });
13
+
14
+ vi.mock("@earendil-works/pi-ai", async () => {
15
+ const actual = await vi.importActual<typeof import("@earendil-works/pi-ai")>("@earendil-works/pi-ai");
16
+ return { ...actual, completeSimple: vi.fn() };
17
+ });
18
+
19
+ type Handler = (event: any, ctx: any) => any;
20
+ type HandlerMap = Record<string, Handler[]>;
21
+ type CommandMap = Record<string, { handler: (args: string, ctx: any) => any }>;
22
+
23
+ function makeHandlers() {
24
+ const handlers: HandlerMap = {};
25
+ const commands: CommandMap = {};
26
+ const sendMessage = vi.fn();
27
+ const pi = {
28
+ on: (event: string, handler: Handler) => { handlers[event] ??= []; handlers[event].push(handler); },
29
+ registerMessageRenderer: () => undefined,
30
+ registerCommand: (name: string, command: { handler: (args: string, ctx: any) => any }) => { commands[name] = command; },
31
+ registerTool: vi.fn(),
32
+ sendMessage,
33
+ sendUserMessage: () => undefined,
34
+ ui: { setStatus: () => undefined, notify: () => undefined },
35
+ };
36
+ return { handlers, commands, pi: pi as any, sendMessage };
37
+ }
38
+
39
+ const ADVISOR_STATE_DIR = join(homedir(), ".pi", "agent", "pi-rogue", "advisor");
40
+ const ADVISOR_STATE_PATH = join(ADVISOR_STATE_DIR, "state.json");
41
+ const ADVISOR_CONFIG_PATH = join(ADVISOR_STATE_DIR, "config.json");
42
+
43
+ function readAdvisorState(): any {
44
+ return JSON.parse(readFileSync(ADVISOR_STATE_PATH, "utf8"));
45
+ }
46
+
47
+ function mkCtx() {
48
+ return {
49
+ sessionManager: {
50
+ getSessionFile: () => join(homedir(), ".pi", "agent", "pi-rogue", "advisor", "session.jsonl"),
51
+ },
52
+ isIdle: () => true,
53
+ modelRegistry: {
54
+ find: (provider: string, model: string) => {
55
+ if (provider === "openai-codex") return { id: `${provider}/${model}`, provider, input: ["text"] };
56
+ return null;
57
+ },
58
+ getAvailable: () => [{ id: "provider/text-light", provider: "provider", input: ["text"] }],
59
+ getApiKeyAndHeaders: async () => ({ ok: true, apiKey: "k", headers: {} }),
60
+ },
61
+ ui: { setStatus: () => undefined, notify: () => undefined },
62
+ } as any;
63
+ }
64
+
65
+ describe("state versioning and recovery", () => {
66
+ let priorState: string | null = null;
67
+ let priorConfig: string | null = null;
68
+
69
+ beforeEach(() => {
70
+ priorState = existsSync(ADVISOR_STATE_PATH) ? readFileSync(ADVISOR_STATE_PATH, "utf8") : null;
71
+ priorConfig = existsSync(ADVISOR_CONFIG_PATH) ? readFileSync(ADVISOR_CONFIG_PATH, "utf8") : null;
72
+
73
+ const setup = makeHandlers();
74
+ const { handlers, commands, pi } = setup;
75
+ handlers; commands;
76
+
77
+ mkdirSync(dirname(ADVISOR_STATE_PATH), { recursive: true });
78
+ writeFileSync(ADVISOR_CONFIG_PATH, JSON.stringify({ mode: "auto", review: "light", checkins: "off", checkinIntervalMinutes: 30 }, null, 2), "utf8");
79
+ writeFileSync(ADVISOR_STATE_PATH, JSON.stringify({
80
+ turns: 0,
81
+ lastTask: "",
82
+ notes: [],
83
+ files: [],
84
+ errors: [],
85
+ advisorCalls: 0,
86
+ cacheHits: 0,
87
+ followUp: "",
88
+ router: {},
89
+ checkin: { queued: false },
90
+ reviewControl: { status: "idle", pending: false, consumed: true, running: false },
91
+ }, null, 2), "utf8");
92
+
93
+ registerAdvisor(pi);
94
+ });
95
+
96
+ afterEach(() => {
97
+ if (priorState === null) {
98
+ writeFileSync(ADVISOR_STATE_PATH, "{}", "utf8");
99
+ } else {
100
+ writeFileSync(ADVISOR_STATE_PATH, priorState, "utf8");
101
+ }
102
+ if (priorConfig === null) {
103
+ writeFileSync(ADVISOR_CONFIG_PATH, "{}", "utf8");
104
+ } else {
105
+ writeFileSync(ADVISOR_CONFIG_PATH, priorConfig, "utf8");
106
+ }
107
+ });
108
+
109
+ it("loads state with version field and preserves existing data", () => {
110
+ // Write a state without _v to simulate old state
111
+ writeFileSync(ADVISOR_STATE_PATH, JSON.stringify({
112
+ turns: 0,
113
+ lastTask: "",
114
+ notes: [],
115
+ files: [],
116
+ errors: [],
117
+ advisorCalls: 0,
118
+ cacheHits: 0,
119
+ followUp: "",
120
+ router: {},
121
+ checkin: { queued: false },
122
+ reviewControl: { status: "idle", pending: false, consumed: true, running: false },
123
+ }, null, 2), "utf8");
124
+
125
+ // Load state directly (simulates what loadState does)
126
+ const raw = JSON.parse(readFileSync(ADVISOR_STATE_PATH, "utf8"));
127
+ expect(raw._v).toBeUndefined(); // Old state has no _v
128
+
129
+ // After loadState + saveState cycle, _v should be present
130
+ const setup = makeHandlers();
131
+ const { handlers: h, pi } = setup;
132
+ registerAdvisor(pi);
133
+ const ctx = mkCtx();
134
+ void h.session_start?.[0]?.({}, ctx);
135
+
136
+ const state = JSON.parse(readFileSync(ADVISOR_STATE_PATH, "utf8"));
137
+ // The session_start handler calls loadState() which adds _v, then saveState() writes it
138
+ expect(state._v).toBe(1);
139
+ expect(state.turns).toBe(0);
140
+ expect(state.lastTask).toBe("");
141
+ });
142
+
143
+ it("recovers corrupted state gracefully", () => {
144
+ // Write corrupted state
145
+ writeFileSync(ADVISOR_STATE_PATH, "{ corrupted json", "utf8");
146
+ const handlers = makeHandlers();
147
+ const { handlers: h, pi } = handlers;
148
+ registerAdvisor(pi);
149
+ // Loading state should not throw
150
+ const ctx = mkCtx();
151
+ void h.session_start?.[0]?.({}, ctx);
152
+ // State should be recovered to default
153
+ const recovered = readAdvisorState();
154
+ expect(recovered._v).toBe(1);
155
+ expect(recovered.turns).toBe(0);
156
+ });
157
+
158
+ it("migrates old state without _v field to current version", () => {
159
+ // Write state without _v field
160
+ writeFileSync(ADVISOR_STATE_PATH, JSON.stringify({
161
+ turns: 5,
162
+ lastTask: "old task",
163
+ notes: [],
164
+ files: [],
165
+ errors: [],
166
+ advisorCalls: 3,
167
+ cacheHits: 1,
168
+ followUp: "",
169
+ router: {},
170
+ checkin: { queued: false },
171
+ reviewControl: { status: "idle", pending: false, consumed: true, running: false },
172
+ }, null, 2), "utf8");
173
+
174
+ const handlers = makeHandlers();
175
+ const { handlers: h, pi } = handlers;
176
+ registerAdvisor(pi);
177
+ const ctx = mkCtx();
178
+ void h.session_start?.[0]?.({}, ctx);
179
+
180
+ const migrated = readAdvisorState();
181
+ expect(migrated._v).toBe(1);
182
+ expect(migrated.turns).toBe(5);
183
+ expect(migrated.lastTask).toBe("old task");
184
+ expect(migrated.advisorCalls).toBe(3);
185
+ });
186
+
187
+ it("handles missing state file by creating default", () => {
188
+ writeFileSync(ADVISOR_STATE_PATH, "{}", "utf8");
189
+ const handlers = makeHandlers();
190
+ const { handlers: h, pi } = handlers;
191
+ registerAdvisor(pi);
192
+ const ctx = mkCtx();
193
+ void h.session_start?.[0]?.({}, ctx);
194
+
195
+ const loaded = readAdvisorState();
196
+ expect(loaded._v).toBe(1);
197
+ expect(loaded.turns).toBe(0);
198
+ expect(loaded.reviewControl.status).toBe("idle");
199
+ });
200
+
201
+ it("preserves reviewControl state across loads", () => {
202
+ const state = readAdvisorState();
203
+ state.reviewControl = {
204
+ status: "needed",
205
+ pending: true,
206
+ consumed: false,
207
+ running: false,
208
+ lastDecision: "review",
209
+ lastMaterialSignature: "test-sig",
210
+ lastReason: "test reason",
211
+ lastTrigger: "turn-1",
212
+ lastAppliedAt: new Date().toISOString(),
213
+ };
214
+ writeFileSync(ADVISOR_STATE_PATH, JSON.stringify(state, null, 2), "utf8");
215
+
216
+ const handlers = makeHandlers();
217
+ const { handlers: h, pi } = handlers;
218
+ registerAdvisor(pi);
219
+ const ctx = mkCtx();
220
+ void h.session_start?.[0]?.({}, ctx);
221
+
222
+ const recovered = readAdvisorState();
223
+ expect(recovered.reviewControl.status).toBe("needed");
224
+ expect(recovered.reviewControl.pending).toBe(true);
225
+ expect(recovered.reviewControl.lastDecision).toBe("review");
226
+ });
227
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fiale-plus/pi-rogue-bundle",
3
- "version": "0.1.13",
3
+ "version": "0.1.14",
4
4
  "description": "Public Pi-Rogue bundle for advisor and orchestration. Single consolidated artefact (advisor and orchestration releases paused; their packages are private and bundled here).",
5
5
  "type": "module",
6
6
  "license": "MIT",