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
132 changes: 132 additions & 0 deletions src/downshift-core.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
maybeUpshiftAfterCompaction,
restoreStateFromEntries,
statusText,
thresholdReached,
type DownshiftConfig,
type DownshiftState,
type ModelTarget,
Expand Down Expand Up @@ -86,6 +87,43 @@ const ctx = {
describe("downshift core", () => {
beforeEach(() => vi.clearAllMocks());

describe("thresholdReached", () => {
it("detects a token threshold independently of percent usage", () => {
expect(
thresholdReached({ tokens: 10_000, percent: 20 }, { tokens: 10_000 }),
).toBe(true);
expect(
thresholdReached(
{ tokens: 10_000, percent: 20 },
{ tokens: 10_000, percent: 50 },
),
).toBe(true);
});

it("detects a percent threshold independently of token usage", () => {
expect(
thresholdReached({ tokens: null, percent: 50 }, { percent: 50 }),
).toBe(true);
expect(
thresholdReached({ tokens: 10_000, percent: 51 }, { percent: 50 }),
).toBe(true);
});

it("does not reach a threshold when the relevant usage is null", () => {
expect(
thresholdReached({ tokens: null, percent: 99 }, { tokens: 100 }),
).toBe(false);
expect(
thresholdReached({ tokens: 100, percent: null }, { percent: 50 }),
).toBe(false);
});

it("does not reach a threshold when usage is missing", () => {
expect(thresholdReached(undefined, { tokens: 100 })).toBe(false);
expect(thresholdReached(undefined, { percent: 50 })).toBe(false);
});
});

function expectPendingHandoff(
runtime: { state: ReturnType<typeof createState> },
continueAfterHandoff: boolean,
Expand Down Expand Up @@ -205,6 +243,36 @@ describe("downshift core", () => {
expect(deps.switchToTarget).not.toHaveBeenCalled();
});

it("does nothing while paused even when the threshold is reached", async () => {
const deps = createDeps();
const runtime = { state: createState({ paused: true }) };

await maybeDownshift(deps, runtime, ctx, "steer");

expect(deps.sendUserMessage).not.toHaveBeenCalled();
expect(deps.switchToTarget).not.toHaveBeenCalled();
expect(runtime.state).toEqual(createState({ paused: true }));
});

it("does nothing while usage is below the configured threshold", async () => {
const deps = createDeps({
...baseConfig,
threshold: { tokens: 1_000, percent: 50 },
});
const runtime = { state: createState() };

await maybeDownshift(
deps,
runtime,
{ getContextUsage: () => ({ tokens: 999, percent: 49 }) },
"steer",
);

expect(deps.sendUserMessage).not.toHaveBeenCalled();
expect(deps.switchToTarget).not.toHaveBeenCalled();
expect(runtime.state).toEqual(createState());
});

it("sends an immediate handoff when threshold is reached while idle", async () => {
const deps = createDeps();
const runtime = { state: createState() };
Expand Down Expand Up @@ -401,6 +469,23 @@ describe("downshift core", () => {
expect(runtime.state.handoff).toBe("idle");
});

it("does not upshift after compaction when upshift is not configured", async () => {
const deps = createDeps({
...baseConfig,
premiumSource: "explicit",
premium,
upshiftAfterCompaction: false,
});
const runtime = createEconomyRuntime();

await maybeUpshiftAfterCompaction(deps, runtime, {
compactionEntry: {},
});

expect(deps.switchToTarget).not.toHaveBeenCalled();
expect(runtime.state.position).toBe("economy");
});

it("does not upshift after compaction without a compaction entry", async () => {
const deps = createUpshiftDeps();
const runtime = createEconomyRuntime();
Expand Down Expand Up @@ -461,6 +546,53 @@ describe("downshift core", () => {
}
});

it("restores the newest valid state entry", () => {
const restored = restoreStateFromEntries(
[
{
type: "custom",
customType: "downshift-state",
data: createState({ position: "premium" }),
},
{
type: "custom",
customType: "downshift-state",
data: createState({ position: "economy", paused: true }),
},
],
"session-1",
);

expect(restored?.position).toBe("economy");
expect(restored?.paused).toBe(true);
});

it("restores legacy session flags", () => {
const restoredOverride = restoreStateFromEntries(
[
{
type: "custom",
customType: "downshift-state",
data: { sessionOverride: true },
},
],
"session-1",
);
const restoredDisabled = restoreStateFromEntries(
[
{
type: "custom",
customType: "downshift-state",
data: { sessionEnabled: false },
},
],
"session-1",
);

expect(restoredOverride?.sessionMode).toBe("on");
expect(restoredDisabled?.sessionMode).toBe("off");
});

it("restores captured premium targets and ignores invalid target shapes", () => {
const restored = restoreStateFromEntries(
[
Expand Down
15 changes: 6 additions & 9 deletions src/downshift-core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,10 @@ function isDownshiftEnabled(
);
}

function hasPendingHandoff(state: DownshiftState): boolean {
return state.handoff === "requested" || state.handoff === "active";
}

function setState(
deps: CoreDeps,
runtime: Runtime,
Expand Down Expand Up @@ -312,10 +316,7 @@ export async function forceDownshiftNow(
deps.notify("downshift: already on economy", "info");
return runtime.state;
}
if (
runtime.state.handoff === "requested" ||
runtime.state.handoff === "active"
) {
if (hasPendingHandoff(runtime.state)) {
deps.notify("downshift: handoff already pending", "info");
return runtime.state;
}
Expand Down Expand Up @@ -399,11 +400,7 @@ export async function maybeDownshift(
runtime.state.position === "economy"
)
return runtime.state;
if (
runtime.state.handoff === "requested" ||
runtime.state.handoff === "active"
)
return runtime.state;
if (hasPendingHandoff(runtime.state)) return runtime.state;
if (!thresholdReached(ctx.getContextUsage(), config.threshold))
return runtime.state;
if (config.handoffBeforeDownshift && runtime.state.handoff === "idle") {
Expand Down
Loading