@astrosheep/pi-context 0.25.2 → 0.26.1
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.
- package/README.md +88 -7
- package/dist/build-info.json +2 -2
- package/dist/extension.js +635 -381
- package/dist/src/context/boot.d.ts +24 -0
- package/dist/src/context/boot.js +33 -24
- package/dist/src/context/budget.d.ts +9 -0
- package/dist/src/context/budget.js +51 -28
- package/dist/src/context/context-window.d.ts +41 -0
- package/dist/src/context/context-window.js +16 -1
- package/dist/src/context/prompts.d.ts +20 -0
- package/dist/src/context/prompts.js +1 -1
- package/dist/src/context/reset-artifacts.d.ts +26 -0
- package/dist/src/context/reset-artifacts.js +18 -17
- package/dist/src/context/reset-lifecycle.d.ts +89 -0
- package/dist/src/context/reset-lifecycle.js +103 -75
- package/dist/src/context/runtime.d.ts +3 -0
- package/dist/src/context/runtime.js +53 -21
- package/dist/src/context/thresholds.d.ts +33 -0
- package/dist/src/context/thresholds.js +1 -1
- package/dist/src/dream/cli.d.ts +10 -0
- package/dist/src/dream/cli.js +1 -1
- package/dist/src/dream/doctor.d.ts +2 -0
- package/dist/src/dream/doctor.js +6 -2
- package/dist/src/dream/gates.d.ts +10 -0
- package/dist/src/dream/git.d.ts +21 -0
- package/dist/src/dream/lock.d.ts +31 -0
- package/dist/src/dream/runner.d.ts +30 -0
- package/dist/src/dream/settings.d.ts +16 -0
- package/dist/src/history/history-tools.d.ts +2 -0
- package/dist/src/history/history.d.ts +57 -0
- package/dist/src/index.d.ts +39 -0
- package/dist/src/index.js +4 -4
- package/dist/src/notes/address.d.ts +26 -0
- package/dist/src/notes/address.js +8 -14
- package/dist/src/notes/constants.d.ts +3 -0
- package/dist/src/notes/constants.js +3 -0
- package/dist/src/notes/context.d.ts +10 -0
- package/dist/src/notes/context.js +33 -0
- package/dist/src/notes/frontmatter.d.ts +46 -0
- package/dist/src/notes/frontmatter.js +10 -5
- package/dist/src/notes/index.d.ts +4 -0
- package/dist/src/notes/index.js +2 -0
- package/dist/src/notes/paths.d.ts +21 -0
- package/dist/src/notes/paths.js +72 -76
- package/dist/src/notes/store.d.ts +94 -0
- package/dist/src/notes/store.js +276 -236
- package/dist/src/pi/notes/adapter.d.ts +12 -0
- package/dist/src/pi/notes/adapter.js +39 -0
- package/dist/src/pi/notes/session-replay.d.ts +16 -0
- package/dist/src/{notes → pi/notes}/session-replay.js +2 -2
- package/dist/src/pi/notes/snapshot.d.ts +33 -0
- package/dist/src/{notes/notes-snapshot.js → pi/notes/snapshot.js} +11 -3
- package/dist/src/pi/notes/tools.d.ts +2 -0
- package/dist/src/{notes → pi/notes}/tools.js +24 -21
- package/dist/src/protocol.d.ts +41 -0
- package/dist/src/protocol.js +4 -6
- package/dist/src/session-reader.d.ts +5 -0
- package/dist/src/settings.d.ts +6 -0
- package/dist/src/text-match.d.ts +5 -0
- package/dist/src/text-match.js +15 -0
- package/dist/src/tool-output.d.ts +97 -0
- package/dist/src/tool-output.js +1 -15
- package/dist/src/tool-schema.d.ts +17 -0
- package/dist/test/agent-loop.test.d.ts +1 -0
- package/dist/test/agent-loop.test.js +387 -20
- package/dist/test/boot.integration.test.d.ts +1 -0
- package/dist/test/boot.integration.test.js +59 -35
- package/dist/test/budget-settings.integration.test.d.ts +1 -0
- package/dist/test/budget-settings.integration.test.js +28 -12
- package/dist/test/doctor.test.d.ts +1 -0
- package/dist/test/doctor.test.js +10 -2
- package/dist/test/dream-skill.test.d.ts +1 -0
- package/dist/test/dream-skill.test.js +69 -0
- package/dist/test/dream.test.d.ts +1 -0
- package/dist/test/helpers/extension-test-environment.d.ts +1 -0
- package/dist/test/helpers/extension-test-environment.js +9 -0
- package/dist/test/helpers/extension.d.ts +104 -0
- package/dist/test/helpers/extension.js +7 -81
- package/dist/test/helpers/notes.d.ts +6 -0
- package/dist/test/helpers/notes.js +13 -0
- package/dist/test/history.integration.test.d.ts +1 -0
- package/dist/test/history.integration.test.js +17 -19
- package/dist/test/notes-library.test.d.ts +1 -0
- package/dist/test/notes-library.test.js +128 -0
- package/dist/test/notes.integration.test.d.ts +1 -0
- package/dist/test/notes.integration.test.js +51 -55
- package/dist/test/notes.test.d.ts +1 -0
- package/dist/test/notes.test.js +162 -34
- package/dist/test/reset-lifecycle.test.d.ts +1 -0
- package/dist/test/reset-lifecycle.test.js +142 -85
- package/docs/architecture.md +8 -8
- package/docs/reset-lifecycle.md +63 -79
- package/package.json +35 -2
- package/playbook.md +33 -32
- package/skills/dream/SKILL.md +12 -0
- package/src/context/boot.ts +44 -25
- package/src/context/budget.ts +52 -27
- package/src/context/context-window.ts +16 -1
- package/src/context/prompts.ts +2 -2
- package/src/context/reset-artifacts.ts +26 -24
- package/src/context/reset-lifecycle.ts +117 -111
- package/src/context/runtime.ts +50 -22
- package/src/context/thresholds.ts +1 -1
- package/src/dream/cli.ts +1 -1
- package/src/dream/doctor.ts +5 -2
- package/src/index.ts +4 -4
- package/src/notes/address.ts +9 -15
- package/src/notes/constants.ts +3 -0
- package/src/notes/context.ts +40 -0
- package/src/notes/frontmatter.ts +18 -12
- package/src/notes/index.ts +22 -0
- package/src/notes/paths.ts +64 -78
- package/src/notes/store.ts +289 -238
- package/src/pi/notes/adapter.ts +44 -0
- package/src/{notes → pi/notes}/session-replay.ts +3 -3
- package/src/{notes/notes-snapshot.ts → pi/notes/snapshot.ts} +13 -4
- package/src/{notes → pi/notes}/tools.ts +25 -23
- package/src/protocol.ts +5 -6
- package/src/text-match.ts +13 -0
- package/src/tool-output.ts +2 -14
|
@@ -6,7 +6,7 @@ import test from "node:test";
|
|
|
6
6
|
import { createAssistantMessageEventStream, getCurrentSystemMessage } from "@earendil-works/pi-ai";
|
|
7
7
|
import { createAgentSession, DefaultResourceLoader, ModelRuntime, SessionManager, SettingsManager, } from "@earendil-works/pi-coding-agent";
|
|
8
8
|
import piContext, { createPiContext } from "../src/index.js";
|
|
9
|
-
import { BOOT_TYPE, CONTEXT_WINDOW_OPEN_TAG, CONTINUATION, CONTINUATION_TYPE, GUIDANCE_OPEN_TAG, GUIDANCE_TYPE, RESET_MARKER_TYPE, WARNING_TYPE } from "../src/protocol.js";
|
|
9
|
+
import { BOOT_TYPE, CONTEXT_WINDOW_OPEN_TAG, CONTINUATION, CONTINUATION_TYPE, GUIDANCE_OPEN_TAG, GUIDANCE_TYPE, RESET_MARKER_TYPE, WARNING_CONTENT, WARNING_PROMPT, WARNING_TYPE } from "../src/protocol.js";
|
|
10
10
|
async function openFixture(options) {
|
|
11
11
|
const dir = options.cwd ?? mkdtempSync(join(tmpdir(), "pi-context-agent-loop-"));
|
|
12
12
|
const ownsDir = options.cwd === undefined;
|
|
@@ -76,7 +76,7 @@ async function openFixture(options) {
|
|
|
76
76
|
}],
|
|
77
77
|
});
|
|
78
78
|
await loader.reload();
|
|
79
|
-
const sessionManager = SessionManager.inMemory(dir);
|
|
79
|
+
const sessionManager = options.sessionManager ?? (options.sessionDir ? SessionManager.create(dir, options.sessionDir) : SessionManager.inMemory(dir));
|
|
80
80
|
options.seed?.(sessionManager);
|
|
81
81
|
const created = await createAgentSession({
|
|
82
82
|
cwd: dir,
|
|
@@ -90,7 +90,7 @@ async function openFixture(options) {
|
|
|
90
90
|
});
|
|
91
91
|
session = created.session;
|
|
92
92
|
session.subscribe((event) => events.push(event));
|
|
93
|
-
session.agent.streamFunction = (_model, context, streamOptions) => {
|
|
93
|
+
session.agent.streamFunction = async (_model, context, streamOptions) => {
|
|
94
94
|
const aborted = streamOptions?.signal?.aborted === true;
|
|
95
95
|
streamContexts.push(context);
|
|
96
96
|
streamSignals.push(aborted);
|
|
@@ -113,7 +113,7 @@ async function openFixture(options) {
|
|
|
113
113
|
}
|
|
114
114
|
requests.push(context);
|
|
115
115
|
const request = requests.length;
|
|
116
|
-
const message = options.script(request, context);
|
|
116
|
+
const message = await options.script(request, context);
|
|
117
117
|
const stream = createAssistantMessageEventStream();
|
|
118
118
|
stream.push({ type: "done", reason: (message.stopReason === "error" || message.stopReason === "aborted" ? "stop" : message.stopReason), message });
|
|
119
119
|
stream.end();
|
|
@@ -127,7 +127,7 @@ async function openFixture(options) {
|
|
|
127
127
|
assert.ok(sessionManager.getBranch().some((entry) => entry.type === "custom" && entry.customType === RESET_MARKER_TYPE && entry.data?.windowId === windowId), "notification follows the reset marker commit");
|
|
128
128
|
assert.ok(sessionManager.getBranch().some((entry) => entry.type === "custom_message" && entry.customType === BOOT_TYPE && entry.details?.windowId === windowId), "notification follows the reset boot commit");
|
|
129
129
|
}
|
|
130
|
-
if (type === "warning" &&
|
|
130
|
+
if (type === "warning" && /^pi-context: Context (?:almost full|running low)/.test(message))
|
|
131
131
|
budgetNotices++;
|
|
132
132
|
notices.push(message);
|
|
133
133
|
},
|
|
@@ -213,30 +213,98 @@ function assertFreshRequest(fixture, requestIndex, oldSentinel) {
|
|
|
213
213
|
assert.ok(body.includes(CONTEXT_WINDOW_OPEN_TAG), "the new provider request includes the fresh context-window boot");
|
|
214
214
|
assert.equal(body.split(CONTINUATION).length - 1, 1, "the fresh window carries exactly one reset message");
|
|
215
215
|
}
|
|
216
|
-
test("real AgentSession: aborted
|
|
216
|
+
test("real AgentSession: aborted early guidance retries silently with automatic reset disabled", async () => {
|
|
217
217
|
let fixture;
|
|
218
218
|
fixture = await openFixture({
|
|
219
219
|
compactionEnabled: false,
|
|
220
220
|
script: (request) => assistant(fixture, [{ type: "text", text: `response ${request}` }], request === 2 ? "aborted" : "stop", { usage: usage(50_000) }),
|
|
221
221
|
});
|
|
222
222
|
try {
|
|
223
|
-
const
|
|
223
|
+
const guidance = () => fixture.sessionManager.getBranch().filter((entry) => entry.type === "custom_message" && entry.customType === GUIDANCE_TYPE);
|
|
224
224
|
const notices = () => fixture.budgetNotices();
|
|
225
225
|
await fixture.session.prompt("Establish usage below the reminder line.");
|
|
226
226
|
await fixture.session.waitForIdle();
|
|
227
|
-
assert.equal(
|
|
227
|
+
assert.equal(guidance().length, 0);
|
|
228
228
|
await fixture.session.prompt("Abort this low-budget request.");
|
|
229
229
|
await fixture.session.waitForIdle();
|
|
230
|
-
assert.equal(
|
|
231
|
-
assert.equal(notices(), 0, "
|
|
230
|
+
assert.equal(guidance().length, 0, "an aborted turn does not commit early guidance");
|
|
231
|
+
assert.equal(notices(), 0, "early guidance never notifies");
|
|
232
232
|
await fixture.session.prompt("Retry successfully.");
|
|
233
233
|
await fixture.session.waitForIdle();
|
|
234
|
-
assert.equal(
|
|
235
|
-
assert.equal(notices(),
|
|
234
|
+
assert.equal(guidance().length, 1, "the successful retry commits the model guidance");
|
|
235
|
+
assert.equal(notices(), 0, "committed early guidance stays UI-silent");
|
|
236
236
|
await fixture.session.prompt("Continue in the same window.");
|
|
237
237
|
await fixture.session.waitForIdle();
|
|
238
|
-
assert.equal(
|
|
239
|
-
assert.equal(notices(),
|
|
238
|
+
assert.equal(guidance().length, 1);
|
|
239
|
+
assert.equal(notices(), 0, "later turns cannot toast the early reminder");
|
|
240
|
+
}
|
|
241
|
+
finally {
|
|
242
|
+
fixture.close();
|
|
243
|
+
}
|
|
244
|
+
});
|
|
245
|
+
test("real AgentSession: an aborted final warning stays silent until a successful retry commits it", async () => {
|
|
246
|
+
let fixture;
|
|
247
|
+
fixture = await openFixture({
|
|
248
|
+
compactionEnabled: true,
|
|
249
|
+
script: (request, context) => {
|
|
250
|
+
if (request === 1)
|
|
251
|
+
return assistant(fixture, [{ type: "toolCall", id: "abort-budget-probe-1", name: "get_context_remaining", arguments: {} }], "toolUse", { usage: usage(50_000) });
|
|
252
|
+
if (request === 2)
|
|
253
|
+
return assistant(fixture, [{ type: "toolCall", id: "abort-budget-probe-2", name: "get_context_remaining", arguments: {} }], "toolUse", { usage: usage(60_000) });
|
|
254
|
+
assert.ok(text(context).includes(WARNING_PROMPT), "the final warning reaches the model on both attempts");
|
|
255
|
+
return assistant(fixture, [{ type: "text", text: `response ${request}` }], request === 3 ? "aborted" : "stop", { usage: usage(60_000) });
|
|
256
|
+
},
|
|
257
|
+
});
|
|
258
|
+
try {
|
|
259
|
+
const warnings = () => fixture.sessionManager.getBranch().filter((entry) => entry.type === "custom_message" && entry.customType === WARNING_TYPE);
|
|
260
|
+
const notices = () => fixture.budgetNotices();
|
|
261
|
+
await fixture.session.prompt("Establish usage and reach the final runway.");
|
|
262
|
+
await fixture.session.waitForIdle();
|
|
263
|
+
assert.equal(fixture.requests.length, 3, "budget probes reach one final-warning attempt");
|
|
264
|
+
assert.equal(warnings().length, 0, "the aborted attempt leaves no durable warning");
|
|
265
|
+
assert.equal(notices(), 0, "an uncommitted warning never notifies");
|
|
266
|
+
await fixture.session.prompt("Retry successfully.");
|
|
267
|
+
await fixture.session.waitForIdle();
|
|
268
|
+
assert.equal(warnings().length, 1, "the successful retry commits one hidden warning");
|
|
269
|
+
assert.equal(resetMarkers(fixture).length, 1, "normal close-out resets after the committed warning");
|
|
270
|
+
assert.equal(notices(), 1, "the committed warning notifies once, including after reset");
|
|
271
|
+
assert.equal(fixture.notices.some((notice) => notice.includes("Context running low")), false, "early guidance remains UI-silent");
|
|
272
|
+
}
|
|
273
|
+
finally {
|
|
274
|
+
fixture.close();
|
|
275
|
+
}
|
|
276
|
+
});
|
|
277
|
+
test("real AgentSession: final warning survives its reset boundary and notifies once", { timeout: 20000 }, async () => {
|
|
278
|
+
let fixture;
|
|
279
|
+
fixture = await openFixture({
|
|
280
|
+
compactionEnabled: true,
|
|
281
|
+
script: (request, context) => {
|
|
282
|
+
if (request === 1)
|
|
283
|
+
return assistant(fixture, [{ type: "toolCall", id: "final-budget-probe-1", name: "get_context_remaining", arguments: {} }], "toolUse", { usage: usage(50_000) });
|
|
284
|
+
if (request === 2)
|
|
285
|
+
return assistant(fixture, [{ type: "toolCall", id: "final-budget-probe-2", name: "get_context_remaining", arguments: {} }], "toolUse", { usage: usage(60_000) });
|
|
286
|
+
if (request === 3) {
|
|
287
|
+
assert.ok(text(context).includes(WARNING_PROMPT), "the final warning reaches the model");
|
|
288
|
+
return assistant(fixture, [{ type: "toolCall", id: "final-budget-wipe", name: "wipe_memory", arguments: {} }], "toolUse", { usage: usage(60_000) });
|
|
289
|
+
}
|
|
290
|
+
return assistant(fixture, [{ type: "text", text: "fresh window ready" }]);
|
|
291
|
+
},
|
|
292
|
+
});
|
|
293
|
+
try {
|
|
294
|
+
assert.equal(fixture.notices.length, 0, "boot is silent");
|
|
295
|
+
await fixture.session.prompt("FINAL_WARNING_RESET_SENTINEL");
|
|
296
|
+
await fixture.session.waitForIdle();
|
|
297
|
+
assert.equal(fixture.requests.length, 4, "two budget probes, close-out tool, and fresh-window request run");
|
|
298
|
+
assertFreshRequest(fixture, 3, "FINAL_WARNING_RESET_SENTINEL");
|
|
299
|
+
assert.equal(fixture.sessionManager.getBranch().filter((entry) => entry.type === "custom_message" && entry.customType === WARNING_TYPE).length, 1);
|
|
300
|
+
assert.equal(resetMarkers(fixture).length, 1, "the reset commits in the same boundary as the final warning");
|
|
301
|
+
assert.equal(fixture.budgetNotices(), 1, "the old-window committed warning reaches the UI after reset");
|
|
302
|
+
assert.equal(fixture.notices.filter((notice) => notice.startsWith("pi-context: memory cleared · ")).length, 1, "reset notification remains intact");
|
|
303
|
+
assert.equal(fixture.notices.some((notice) => notice.includes("Context running low")), false, "early guidance stays silent");
|
|
304
|
+
await fixture.session.prompt("Continue after reset.");
|
|
305
|
+
await fixture.session.waitForIdle();
|
|
306
|
+
assert.equal(fixture.budgetNotices(), 1, "later turns cannot repeat the consumed final warning");
|
|
307
|
+
assert.equal(fixture.notices.filter((notice) => notice.startsWith("pi-context: memory cleared · ")).length, 1, "the prior reset notification is also deduplicated");
|
|
240
308
|
}
|
|
241
309
|
finally {
|
|
242
310
|
fixture.close();
|
|
@@ -268,13 +336,181 @@ test("real AgentSession: explicit tiny-session wipe ignores keepRecentTokens and
|
|
|
268
336
|
const boot = fixture.sessionManager.getBranch().find((entry) => entry.type === "custom_message" && entry.customType === BOOT_TYPE && entry.details && typeof entry.details === "object" && entry.details.windowId === windowId);
|
|
269
337
|
assert.ok(boot && boot.type === "custom_message", "the reset boot is persisted after the marker");
|
|
270
338
|
assert.equal(boot.details.windowId, windowId);
|
|
271
|
-
|
|
339
|
+
const checkpoint = fixture.sessionManager.getBranch().find((entry) => entry.type === "compaction");
|
|
340
|
+
assert.ok(checkpoint && checkpoint.type === "compaction", "explicit wipe commits a native checkpoint without a summary model call");
|
|
341
|
+
assert.equal(checkpoint.summary, "");
|
|
342
|
+
assert.equal(checkpoint.firstKeptEntryId, checkpoint.id, "the checkpoint retains none of the preceding context");
|
|
272
343
|
assert.ok(JSON.stringify(fixture.sessionManager.getBranch()).includes("OLD_CONTEXT_SENTINEL"), "raw history remains readable");
|
|
273
344
|
}
|
|
274
345
|
finally {
|
|
275
346
|
fixture.close();
|
|
276
347
|
}
|
|
277
348
|
});
|
|
349
|
+
test("real AgentSession: manual close-out spans note turns and commits its checkpoint after notes", { timeout: 20000 }, async () => {
|
|
350
|
+
let fixture;
|
|
351
|
+
fixture = await openFixture({
|
|
352
|
+
compactionEnabled: false,
|
|
353
|
+
script: (request, context) => {
|
|
354
|
+
if (request === 1)
|
|
355
|
+
return assistant(fixture, [{ type: "text", text: "before manual close-out" }]);
|
|
356
|
+
if (request === 2) {
|
|
357
|
+
assert.ok(text(context).includes(WARNING_PROMPT), "manual /wipe-memory sends the shared close-out warning");
|
|
358
|
+
return assistant(fixture, [{ type: "toolCall", id: "manual-note-a", name: "notes_write", arguments: { address: "manual-a.md", content: "MANUAL_NOTE_ALPHA" } }], "toolUse");
|
|
359
|
+
}
|
|
360
|
+
if (request === 3)
|
|
361
|
+
return assistant(fixture, [{ type: "toolCall", id: "manual-note-b", name: "notes_write", arguments: { address: "manual-b.md", content: "MANUAL_NOTE_BETA" } }], "toolUse");
|
|
362
|
+
if (request === 4)
|
|
363
|
+
return assistant(fixture, [{ type: "toolCall", id: "manual-wipe", name: "wipe_memory", arguments: {} }], "toolUse");
|
|
364
|
+
assertFreshRequest(fixture, request - 1, "MANUAL_CLOSEOUT_OLD_SENTINEL");
|
|
365
|
+
return assistant(fixture, [{ type: "text", text: "new window resumed" }]);
|
|
366
|
+
},
|
|
367
|
+
});
|
|
368
|
+
try {
|
|
369
|
+
await fixture.session.prompt("MANUAL_CLOSEOUT_OLD_SENTINEL");
|
|
370
|
+
await fixture.session.waitForIdle();
|
|
371
|
+
await fixture.session.prompt("/wipe-memory");
|
|
372
|
+
await fixture.session.waitForIdle();
|
|
373
|
+
assert.equal(fixture.requests.length, 5, "two note turns, explicit wipe, and one fresh continuation run");
|
|
374
|
+
assert.equal(resetMarkers(fixture).length, 1);
|
|
375
|
+
const branch = fixture.sessionManager.getBranch();
|
|
376
|
+
const warningIndex = branch.findIndex((entry) => entry.type === "custom_message" && entry.customType === WARNING_TYPE);
|
|
377
|
+
const checkpointIndex = branch.findIndex((entry) => entry.type === "compaction");
|
|
378
|
+
const markerIndex = branch.findIndex((entry) => entry.type === "custom" && entry.customType === RESET_MARKER_TYPE);
|
|
379
|
+
assert.ok(warningIndex >= 0 && warningIndex < checkpointIndex, "the warning is durable before the checkpoint");
|
|
380
|
+
const warning = branch[warningIndex];
|
|
381
|
+
assert.ok(warning?.type === "custom_message");
|
|
382
|
+
assert.equal(warning.content, WARNING_CONTENT, "manual and budget close-out persist the same hidden warning");
|
|
383
|
+
assert.equal(branch[markerIndex - 1]?.id, branch[checkpointIndex]?.id, "native retain-none checkpoint immediately precedes the marker");
|
|
384
|
+
assert.ok(branch.slice(0, checkpointIndex).filter((entry) => entry.type === "message" && entry.message.role === "toolResult" && entry.message.toolName === "notes_write").length >= 2, "both note writes finish before the checkpoint");
|
|
385
|
+
const marker = resetMarkers(fixture)[0];
|
|
386
|
+
assert.equal(marker.type, "custom");
|
|
387
|
+
const windowId = marker.data.windowId;
|
|
388
|
+
const boot = branch.find((entry) => entry.type === "custom_message" && entry.customType === BOOT_TYPE && entry.details?.windowId === windowId);
|
|
389
|
+
assert.ok(boot && boot.type === "custom_message");
|
|
390
|
+
assert.ok(typeof boot.content === "string" && boot.content.includes("manual-a.md") && boot.content.includes("manual-b.md"), "the fresh boot indexes both completed notes");
|
|
391
|
+
assert.equal(boot.display, false);
|
|
392
|
+
const canonical = fixture.sessionManager.buildSessionProjection().messages;
|
|
393
|
+
assert.equal(JSON.stringify(canonical).includes("MANUAL_CLOSEOUT_OLD_SENTINEL"), false, "canonical context excludes the prior window");
|
|
394
|
+
assert.ok(canonical.some((message) => message.role === "compactionSummary"), "the empty native summary wrapper remains canonical");
|
|
395
|
+
assert.ok(JSON.stringify(branch).includes("MANUAL_CLOSEOUT_OLD_SENTINEL"), "raw history remains intact");
|
|
396
|
+
}
|
|
397
|
+
finally {
|
|
398
|
+
fixture.close();
|
|
399
|
+
}
|
|
400
|
+
});
|
|
401
|
+
test("real AgentSession: repeated manual commands share one pending window and normal-stop fallback", { timeout: 20000 }, async () => {
|
|
402
|
+
let fixture;
|
|
403
|
+
let announceStarted;
|
|
404
|
+
let releaseResponse;
|
|
405
|
+
const started = new Promise((resolve) => { announceStarted = resolve; });
|
|
406
|
+
const gate = new Promise((resolve) => { releaseResponse = resolve; });
|
|
407
|
+
fixture = await openFixture({
|
|
408
|
+
compactionEnabled: false,
|
|
409
|
+
script: async (request) => {
|
|
410
|
+
if (request === 1) {
|
|
411
|
+
announceStarted();
|
|
412
|
+
await gate;
|
|
413
|
+
}
|
|
414
|
+
return assistant(fixture, [{ type: "text", text: "close-out complete" }]);
|
|
415
|
+
},
|
|
416
|
+
});
|
|
417
|
+
try {
|
|
418
|
+
const first = fixture.session.prompt("/wipe-memory");
|
|
419
|
+
await started;
|
|
420
|
+
const second = fixture.session.prompt("/wipe-memory");
|
|
421
|
+
releaseResponse();
|
|
422
|
+
await Promise.all([first, second]);
|
|
423
|
+
await fixture.session.waitForIdle();
|
|
424
|
+
assert.equal(fixture.requests.length, 2, "duplicate command waits do not schedule a second fresh-window request");
|
|
425
|
+
assert.equal(fixture.sessionManager.getBranch().filter((entry) => entry.type === "custom_message" && entry.customType === WARNING_TYPE).length, 1, "the same pending-window warning is deduplicated");
|
|
426
|
+
assert.equal(resetMarkers(fixture).length, 1, "normal stop commits exactly one reset");
|
|
427
|
+
assert.equal(fixture.sessionManager.getBranch().filter((entry) => entry.type === "compaction").length, 1);
|
|
428
|
+
}
|
|
429
|
+
finally {
|
|
430
|
+
releaseResponse();
|
|
431
|
+
fixture.close();
|
|
432
|
+
}
|
|
433
|
+
});
|
|
434
|
+
test("real AgentSession: user abort, synthetic aborted response, and generic error never fall back", { timeout: 25000 }, async () => {
|
|
435
|
+
for (const mode of ["ctx-abort", "synthetic-abort", "generic-error"]) {
|
|
436
|
+
let fixture;
|
|
437
|
+
let didAbort = false;
|
|
438
|
+
fixture = await openFixture({
|
|
439
|
+
compactionEnabled: false,
|
|
440
|
+
hook: mode === "ctx-abort" ? (pi) => pi.on("context", (_event, ctx) => {
|
|
441
|
+
if (!didAbort) {
|
|
442
|
+
didAbort = true;
|
|
443
|
+
ctx.abort();
|
|
444
|
+
}
|
|
445
|
+
}) : undefined,
|
|
446
|
+
script: (request) => {
|
|
447
|
+
if (request === 1 && mode === "synthetic-abort")
|
|
448
|
+
return assistant(fixture, [{ type: "text", text: "synthetic aborted" }], "aborted");
|
|
449
|
+
if (request === 1 && mode === "generic-error")
|
|
450
|
+
return assistant(fixture, [{ type: "text", text: "request failed" }], "error", { errorMessage: "synthetic generic provider failure" });
|
|
451
|
+
return assistant(fixture, [{ type: "text", text: "later user turn" }]);
|
|
452
|
+
},
|
|
453
|
+
});
|
|
454
|
+
try {
|
|
455
|
+
await fixture.session.prompt("/wipe-memory");
|
|
456
|
+
await fixture.session.waitForIdle();
|
|
457
|
+
assert.equal(resetMarkers(fixture).length, 0, `${mode} does not force a reset`);
|
|
458
|
+
assert.equal(fixture.sessionManager.getBranch().filter((entry) => entry.type === "compaction").length, 0);
|
|
459
|
+
assert.equal(fixture.notices.some((notice) => notice.includes("memory cleared")), false);
|
|
460
|
+
if (mode === "ctx-abort")
|
|
461
|
+
assert.equal(fixture.streamSignals.at(-1), true, "this case aborts the real Pi operation signal");
|
|
462
|
+
else
|
|
463
|
+
assert.equal(fixture.streamSignals.at(-1), false, "synthetic abort/error is not a user abort signal");
|
|
464
|
+
await fixture.session.prompt("AFTER_UNSUCCESSFUL_CLOSEOUT");
|
|
465
|
+
await fixture.session.waitForIdle();
|
|
466
|
+
assert.equal(resetMarkers(fixture).length, 0, "no pending close-out leaks into a later prompt");
|
|
467
|
+
}
|
|
468
|
+
finally {
|
|
469
|
+
fixture.close();
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
});
|
|
473
|
+
test("real AgentSession: steering and follow-up queued during fallback reach the old window once", { timeout: 25000 }, async () => {
|
|
474
|
+
for (const delivery of ["steer", "followUp"]) {
|
|
475
|
+
let fixture;
|
|
476
|
+
let announceStarted;
|
|
477
|
+
let releaseResponse;
|
|
478
|
+
const started = new Promise((resolve) => { announceStarted = resolve; });
|
|
479
|
+
const gate = new Promise((resolve) => { releaseResponse = resolve; });
|
|
480
|
+
fixture = await openFixture({
|
|
481
|
+
compactionEnabled: false,
|
|
482
|
+
script: async (request, context) => {
|
|
483
|
+
if (request === 1) {
|
|
484
|
+
assert.ok(text(context).includes(WARNING_PROMPT));
|
|
485
|
+
announceStarted();
|
|
486
|
+
await gate;
|
|
487
|
+
}
|
|
488
|
+
if (request === 2)
|
|
489
|
+
assert.equal(text(context).split("QUEUED_DURING_FALLBACK").length - 1, 1, `${delivery} is delivered once before fallback`);
|
|
490
|
+
return assistant(fixture, [{ type: "text", text: `response ${request}` }]);
|
|
491
|
+
},
|
|
492
|
+
});
|
|
493
|
+
try {
|
|
494
|
+
const command = fixture.session.prompt("/wipe-memory");
|
|
495
|
+
await started;
|
|
496
|
+
if (delivery === "steer")
|
|
497
|
+
fixture.session.steer("QUEUED_DURING_FALLBACK");
|
|
498
|
+
else
|
|
499
|
+
fixture.session.followUp("QUEUED_DURING_FALLBACK");
|
|
500
|
+
releaseResponse();
|
|
501
|
+
await command;
|
|
502
|
+
await fixture.session.waitForIdle();
|
|
503
|
+
assert.equal(fixture.requests.length, 3, `${delivery} runs once before one fresh continuation`);
|
|
504
|
+
assert.equal(resetMarkers(fixture).length, 1, `${delivery} does not duplicate the fallback reset`);
|
|
505
|
+
assert.equal(fixture.sessionManager.getBranch().filter((entry) => entry.type === "message" && JSON.stringify(entry.message).includes("QUEUED_DURING_FALLBACK")).length, 1);
|
|
506
|
+
assertFreshRequest(fixture, 2, "QUEUED_DURING_FALLBACK");
|
|
507
|
+
}
|
|
508
|
+
finally {
|
|
509
|
+
releaseResponse();
|
|
510
|
+
fixture.close();
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
});
|
|
278
514
|
test("real AgentSession: a reset survives all notes-home read failures with an incomplete boot", { timeout: 15000 }, async () => {
|
|
279
515
|
let fixture;
|
|
280
516
|
fixture = await openFixture({
|
|
@@ -390,6 +626,66 @@ test("real AgentSession: successive resets and a mixed tool batch cut only after
|
|
|
390
626
|
}
|
|
391
627
|
}
|
|
392
628
|
});
|
|
629
|
+
test("real AgentSession: two retain-none checkpoints survive session-file reopen", { timeout: 20000 }, async () => {
|
|
630
|
+
const sessionDir = mkdtempSync(join(tmpdir(), "pi-context-persisted-session-"));
|
|
631
|
+
let fixture;
|
|
632
|
+
let sessionFile = "";
|
|
633
|
+
let sessionId = "";
|
|
634
|
+
fixture = await openFixture({
|
|
635
|
+
compactionEnabled: false,
|
|
636
|
+
sessionDir,
|
|
637
|
+
script: (request) => request < 3
|
|
638
|
+
? assistant(fixture, [{ type: "toolCall", id: `persisted-wipe-${request}`, name: "wipe_memory", arguments: {} }], "toolUse")
|
|
639
|
+
: assistant(fixture, [{ type: "text", text: "second reset window complete" }]),
|
|
640
|
+
});
|
|
641
|
+
try {
|
|
642
|
+
await fixture.session.prompt("PERSISTED_OLD_WINDOW_SENTINEL");
|
|
643
|
+
await fixture.session.waitForIdle();
|
|
644
|
+
assert.equal(fixture.requests.length, 3, "two explicit requests and one final continuation, with no summary-model request");
|
|
645
|
+
assert.equal(resetMarkers(fixture).length, 2);
|
|
646
|
+
assertFreshRequest(fixture, 2, "PERSISTED_OLD_WINDOW_SENTINEL");
|
|
647
|
+
const branch = fixture.sessionManager.getBranch();
|
|
648
|
+
const checkpoints = branch.filter((entry) => entry.type === "compaction");
|
|
649
|
+
assert.equal(checkpoints.length, 2);
|
|
650
|
+
const markers = resetMarkers(fixture);
|
|
651
|
+
assert.equal(markers[0]?.parentId, checkpoints[0]?.id);
|
|
652
|
+
assert.equal(markers[1]?.parentId, checkpoints[1]?.id);
|
|
653
|
+
assert.equal(checkpoints[0]?.type === "compaction" ? checkpoints[0].firstKeptEntryId : undefined, checkpoints[0]?.id);
|
|
654
|
+
assert.equal(checkpoints[1]?.type === "compaction" ? checkpoints[1].firstKeptEntryId : undefined, checkpoints[1]?.id);
|
|
655
|
+
sessionFile = fixture.sessionManager.getSessionFile() ?? "";
|
|
656
|
+
sessionId = fixture.sessionManager.getSessionId();
|
|
657
|
+
assert.ok(sessionFile && existsSync(sessionFile), "the SDK persisted the real session file");
|
|
658
|
+
}
|
|
659
|
+
finally {
|
|
660
|
+
fixture.close();
|
|
661
|
+
}
|
|
662
|
+
try {
|
|
663
|
+
const reopened = SessionManager.open(sessionFile, sessionDir);
|
|
664
|
+
assert.equal(reopened.getSessionId(), sessionId);
|
|
665
|
+
const branch = reopened.getBranch();
|
|
666
|
+
assert.equal(branch.filter((entry) => entry.type === "compaction").length, 2);
|
|
667
|
+
assert.equal(branch.filter((entry) => entry.type === "custom" && entry.customType === RESET_MARKER_TYPE).length, 2);
|
|
668
|
+
assert.ok(JSON.stringify(branch).includes("PERSISTED_OLD_WINDOW_SENTINEL"), "raw history survives disk reopen");
|
|
669
|
+
const canonical = reopened.buildSessionProjection().messages;
|
|
670
|
+
assert.equal(JSON.stringify(canonical).includes("PERSISTED_OLD_WINDOW_SENTINEL"), false, "disk-loaded canonical context agrees with the fresh request");
|
|
671
|
+
assert.ok(canonical.some((message) => message.role === "compactionSummary"), "the accepted empty-summary wrapper survives reload");
|
|
672
|
+
const system = getCurrentSystemMessage(canonical);
|
|
673
|
+
assert.ok(JSON.stringify(system).includes("Use the tools as requested."), "the complete system prompt survives the checkpoint");
|
|
674
|
+
assert.ok(system?.toolsAdded?.some((tool) => tool.name === "wipe_memory"), "the active tool loadout survives reopen");
|
|
675
|
+
const reopenedMarkers = branch.filter((entry) => entry.type === "custom" && entry.customType === RESET_MARKER_TYPE);
|
|
676
|
+
const latestMarker = reopenedMarkers.at(-1);
|
|
677
|
+
const previousMarker = reopenedMarkers[0];
|
|
678
|
+
assert.ok(latestMarker?.type === "custom" && previousMarker?.type === "custom");
|
|
679
|
+
const latestWindowId = latestMarker.data.windowId;
|
|
680
|
+
const previousWindowId = previousMarker.data.windowId;
|
|
681
|
+
const latestBoot = branch.find((entry) => entry.type === "custom_message" && entry.customType === BOOT_TYPE && entry.details?.windowId === latestWindowId);
|
|
682
|
+
assert.ok(latestBoot && latestBoot.type === "custom_message" && typeof latestBoot.content === "string");
|
|
683
|
+
assert.ok(latestBoot.content.includes(`Previous context window id: ${previousWindowId}`));
|
|
684
|
+
}
|
|
685
|
+
finally {
|
|
686
|
+
rmSync(sessionDir, { recursive: true, force: true });
|
|
687
|
+
}
|
|
688
|
+
});
|
|
393
689
|
test("real AgentSession: steering and follow-up are delivered exactly once in the fresh window", async () => {
|
|
394
690
|
for (const deliverAs of ["steer", "followUp"]) {
|
|
395
691
|
let fixture;
|
|
@@ -531,6 +827,7 @@ test("real AgentSession: overflow and recoverable length reset and retry once; r
|
|
|
531
827
|
await fixture.session.prompt(`RECOVER_${failure}_SENTINEL`);
|
|
532
828
|
await fixture.session.waitForIdle();
|
|
533
829
|
assert.equal(resetMarkers(fixture).length, 1, `${failure} performs one reset`);
|
|
830
|
+
assert.equal(fixture.sessionManager.getBranch().filter((entry) => entry.type === "compaction").length, 1, `${failure} commits one native reset checkpoint`);
|
|
534
831
|
assert.equal(fixture.requests.length, 2, `${failure} resumes once`);
|
|
535
832
|
assertFreshRequest(fixture, 1, `RECOVER_${failure}_SENTINEL`);
|
|
536
833
|
}
|
|
@@ -546,8 +843,8 @@ test("real AgentSession: overflow and recoverable length reset and retry once; r
|
|
|
546
843
|
await repeated.session.prompt("REPEATED_OVERFLOW_SENTINEL");
|
|
547
844
|
await repeated.session.waitForIdle();
|
|
548
845
|
assert.equal(resetMarkers(repeated).length, 1, "repeated overflow does not create repeated windows");
|
|
846
|
+
assert.equal(repeated.sessionManager.getBranch().filter((entry) => entry.type === "compaction").length, 1, "bounded overflow recovery commits one retain-none checkpoint");
|
|
549
847
|
assert.equal(repeated.requests.length, 2, "repeated overflow stops after one recovery request");
|
|
550
|
-
assert.ok(repeated.events.some((event) => event.type === "compaction_end" && typeof event.errorMessage === "string"));
|
|
551
848
|
}
|
|
552
849
|
finally {
|
|
553
850
|
repeated.close();
|
|
@@ -709,7 +1006,7 @@ test("real AgentSession: injected settings managers own live policy and ignore c
|
|
|
709
1006
|
await first.session.waitForIdle();
|
|
710
1007
|
assert.equal(resetMarkers(first).length, 1, "live enablement and a lower reserve apply on the next turn");
|
|
711
1008
|
firstHighUsage.next = true;
|
|
712
|
-
firstManager.applyOverrides({ compaction: { reserveTokens:
|
|
1009
|
+
firstManager.applyOverrides({ compaction: { reserveTokens: 80_000 } });
|
|
713
1010
|
await first.session.prompt("INJECTED_LIVE_HIGH_RESERVE");
|
|
714
1011
|
await first.session.waitForIdle();
|
|
715
1012
|
assert.equal(resetMarkers(first).length, 2, "a live reserve increase drives the next reset decision");
|
|
@@ -781,14 +1078,83 @@ test("real AgentSession: injected model overrides select the active model's rese
|
|
|
781
1078
|
rmSync(notesRoot, { recursive: true, force: true });
|
|
782
1079
|
}
|
|
783
1080
|
});
|
|
1081
|
+
test("real AgentSession: persisted budget warning re-arms after a later abort/error", { timeout: 30000 }, async () => {
|
|
1082
|
+
for (const mode of ["ctx-abort", "generic-error"]) {
|
|
1083
|
+
let fixture;
|
|
1084
|
+
let warningTurnStarted;
|
|
1085
|
+
let releaseWarningTurn;
|
|
1086
|
+
let didAbort = false;
|
|
1087
|
+
const warningStarted = new Promise((resolve) => { warningTurnStarted = resolve; });
|
|
1088
|
+
const warningGate = new Promise((resolve) => { releaseWarningTurn = resolve; });
|
|
1089
|
+
fixture = await openFixture({
|
|
1090
|
+
compactionEnabled: true,
|
|
1091
|
+
hook: mode === "ctx-abort" ? (pi, _getSession, requests) => pi.on("context", (_event, ctx) => {
|
|
1092
|
+
if (!didAbort && requests.length === 3) {
|
|
1093
|
+
didAbort = true;
|
|
1094
|
+
ctx.abort();
|
|
1095
|
+
}
|
|
1096
|
+
}) : undefined,
|
|
1097
|
+
script: async (request, context) => {
|
|
1098
|
+
if (request === 1)
|
|
1099
|
+
return assistant(fixture, [{ type: "toolCall", id: "rearm-probe-1", name: "get_context_remaining", arguments: {} }], "toolUse", { usage: usage(50_000) });
|
|
1100
|
+
if (request === 2)
|
|
1101
|
+
return assistant(fixture, [{ type: "toolCall", id: "rearm-probe-2", name: "get_context_remaining", arguments: {} }], "toolUse", { usage: usage(60_000) });
|
|
1102
|
+
if (request === 3) {
|
|
1103
|
+
assert.ok(text(context).includes(WARNING_PROMPT), "the low-budget request carries the close-out warning");
|
|
1104
|
+
warningTurnStarted();
|
|
1105
|
+
await warningGate;
|
|
1106
|
+
return assistant(fixture, [{ type: "toolCall", id: "rearm-note", name: "notes_write", arguments: { address: "warning-rearm.md", content: "DURABLE_WARNING_CHECKPOINT" } }], "toolUse", { usage: usage(60_000) });
|
|
1107
|
+
}
|
|
1108
|
+
if (request === 4 && mode === "generic-error") {
|
|
1109
|
+
assert.equal(text(context).split(WARNING_PROMPT).length - 1, 1, "the durable warning is not injected a second time");
|
|
1110
|
+
assert.ok(text(context).includes("REARM_QUEUED_FOLLOWUP"));
|
|
1111
|
+
return assistant(fixture, [{ type: "text", text: "ordinary provider failure" }], "error", { errorMessage: "synthetic non-overflow failure", usage: usage(60_000) });
|
|
1112
|
+
}
|
|
1113
|
+
return assistant(fixture, [{ type: "text", text: `normal response ${request}` }], "stop", { usage: usage(60_000) });
|
|
1114
|
+
},
|
|
1115
|
+
});
|
|
1116
|
+
try {
|
|
1117
|
+
const initialTurn = fixture.session.prompt(`REARM_${mode}_INITIAL`);
|
|
1118
|
+
await warningStarted;
|
|
1119
|
+
fixture.session.steer("REARM_QUEUED_FOLLOWUP");
|
|
1120
|
+
releaseWarningTurn();
|
|
1121
|
+
await initialTurn;
|
|
1122
|
+
await fixture.session.waitForIdle();
|
|
1123
|
+
const afterFailure = fixture.sessionManager.getBranch();
|
|
1124
|
+
assert.equal(afterFailure.filter((entry) => entry.type === "custom_message" && entry.customType === WARNING_TYPE).length, 1, "the warning was persisted exactly once before the failed follow-up");
|
|
1125
|
+
assert.ok(existsSync(join(fixture.notesRoot, "pi", "session", fixture.sessionManager.getSessionId(), "warning-rearm.md")), "the preceding note/tool turn completed");
|
|
1126
|
+
assert.equal(resetMarkers(fixture).length, 0, `${mode} itself must not reset`);
|
|
1127
|
+
assert.equal(afterFailure.filter((entry) => entry.type === "compaction").length, 0);
|
|
1128
|
+
const failedContext = fixture.streamContexts[3];
|
|
1129
|
+
assert.ok(failedContext);
|
|
1130
|
+
assert.equal(text(failedContext).split(WARNING_PROMPT).length - 1, 1, "the persisted warning appears once, without duplicate injection");
|
|
1131
|
+
assert.equal(text(failedContext).split("REARM_QUEUED_FOLLOWUP").length - 1, 1, "queued follow-up reaches the failed request once");
|
|
1132
|
+
assert.equal(fixture.streamSignals.at(-1), mode === "ctx-abort", "the abort case cancels Pi's real signal; the error case does not");
|
|
1133
|
+
await fixture.session.prompt("REARM_AFTER_FAILED_TURN");
|
|
1134
|
+
await fixture.session.waitForIdle();
|
|
1135
|
+
assert.equal(resetMarkers(fixture).length, 1, "the next low-budget normal stop re-arms and commits one reset");
|
|
1136
|
+
assert.equal(fixture.sessionManager.getBranch().filter((entry) => entry.type === "custom_message" && entry.customType === WARNING_TYPE).length, 1, "re-arming does not duplicate the durable warning");
|
|
1137
|
+
const checkpoint = fixture.sessionManager.getBranch().find((entry) => entry.type === "compaction");
|
|
1138
|
+
assert.ok(checkpoint?.type === "compaction");
|
|
1139
|
+
assert.equal(checkpoint.firstKeptEntryId, checkpoint.id, "the retry closes with one retain-none checkpoint");
|
|
1140
|
+
assertFreshRequest(fixture, fixture.requests.length - 1, "REARM_AFTER_FAILED_TURN");
|
|
1141
|
+
}
|
|
1142
|
+
finally {
|
|
1143
|
+
releaseWarningTurn();
|
|
1144
|
+
fixture.close();
|
|
1145
|
+
}
|
|
1146
|
+
}
|
|
1147
|
+
});
|
|
784
1148
|
test("real AgentSession: warning precedes a durable checkpoint, including failed writes and an ignored warning", { timeout: 20000 }, async () => {
|
|
785
1149
|
for (const scenario of ["checkpoint", "write-error", "ignored-warning"]) {
|
|
786
1150
|
let fixture;
|
|
787
1151
|
fixture = await openFixture({
|
|
788
1152
|
compactionEnabled: true,
|
|
789
1153
|
script: (request, context) => {
|
|
790
|
-
if (request === 3)
|
|
791
|
-
assert.ok(text(context).includes(GUIDANCE_OPEN_TAG), "the
|
|
1154
|
+
if (request === 3) {
|
|
1155
|
+
assert.ok(text(context).includes(GUIDANCE_OPEN_TAG), "the warning remains in its hidden guidance wrapper");
|
|
1156
|
+
assert.ok(text(context).includes(WARNING_PROMPT), "the shared close-out warning reaches the provider before checkpoint choice");
|
|
1157
|
+
}
|
|
792
1158
|
if (request === 1)
|
|
793
1159
|
return assistant(fixture, [{ type: "toolCall", id: "budget-probe-1", name: "get_context_remaining", arguments: {} }], "toolUse", { usage: usage(50_000) });
|
|
794
1160
|
if (request === 2)
|
|
@@ -821,8 +1187,9 @@ test("real AgentSession: warning precedes a durable checkpoint, including failed
|
|
|
821
1187
|
assert.ok(branch.some((entry) => entry.type === "message" && entry.message.role === "toolResult" && entry.message.toolName === "notes_write" && entry.message.isError), "the failed write is a durable tool error");
|
|
822
1188
|
}
|
|
823
1189
|
else {
|
|
824
|
-
assert.equal(resetMarkers(fixture).length,
|
|
1190
|
+
assert.equal(resetMarkers(fixture).length, 1, "normal stop falls back to the same reset even when the warning was ignored");
|
|
825
1191
|
assert.equal(existsSync(noteFile), false);
|
|
1192
|
+
assertFreshRequest(fixture, fixture.requests.length - 1, `WARNING_${scenario.toUpperCase()}_SENTINEL`);
|
|
826
1193
|
}
|
|
827
1194
|
}
|
|
828
1195
|
finally {
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|