Files
agent-radar 2ed3415fb7
Publish Claude Code Binaries / Stage and publish binaries (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-arm64 (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build linux-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build windows-amd64 (push) Canceled after 0s
Sync Tutti Agent Skills / sync (push) Canceled after 0s
Windows Agent Adapters / Test Windows agent adapters (push) Canceled after 0s
Windows Daemon Adapters / Test Windows daemon adapters (push) Canceled after 0s
Publish Tutti App Runtime / Publish runtime catalog (push) Canceled after 0s
源码快照 @ 2026-09-01 (source-only, 剔除非源码素材)
2026-09-01 20:17:27 +08:00

211 lines
7.3 KiB
TypeScript

import { describe, expect, it, vi } from "vitest";
import type { AgentActivityComposerOptions } from "@tutti-os/agent-activity-core";
import { ComposerSettingsCore } from "./composerSettingsCore.ts";
import type { ComposerSettingsDraft } from "./types.ts";
function options(
overrides: Partial<AgentActivityComposerOptions> = {}
): AgentActivityComposerOptions {
return {
provider: "codex",
capabilities: null,
models: [
{ label: "GPT-5.6-Sol", value: "gpt-5.6-sol" },
{ label: "GPT-5.6-Terra", value: "gpt-5.6-terra" }
],
reasoningEfforts: [],
speeds: [],
skills: [],
behavior: {
collapseModelOptionsToLatest: false,
modelOptionsAuthoritative: true,
refreshModelOptionsAfterSettings: true,
prewarmDraftSession: false,
planModeExclusiveWithPermissionMode: false
},
effectiveSettings: { model: "gpt-5.6-terra", reasoningEffort: "high" },
loadedAtUnixMs: 1,
...overrides
};
}
interface DeferredFetch {
input: {
agentTargetId: string;
cwd: string | null;
settings: ComposerSettingsDraft | null;
};
resolve(value: AgentActivityComposerOptions): void;
reject(error: Error): void;
}
function createHarness(
rememberDefaults?: (id: string, patch: ComposerSettingsDraft) => Promise<void>
) {
const fetches: DeferredFetch[] = [];
const core = new ComposerSettingsCore(
{
fetchOptions: (input) =>
new Promise((resolve, reject) => {
fetches.push({ input, resolve, reject });
}),
...(rememberDefaults ? { rememberDefaults } : {})
},
{ agentTargetId: "local:codex", cwd: "/repo" }
);
return { core, fetches };
}
async function settled(): Promise<void> {
await Promise.resolve();
await Promise.resolve();
}
describe("ComposerSettingsCore", () => {
it("fences out a slow settings-less response that lands after a newer pick", async () => {
// The regression that motivated this core: the initial context load stalls
// (slow catalog probe), the user picks a model meanwhile, and the stale
// echo-free response must not overwrite the pick's options.
const { core, fetches } = createHarness();
core.refresh(); // initial context load, settings-less
core.setSettings({ model: "gpt-5.6-sol" }); // newer, with settings
expect(fetches).toHaveLength(2);
expect(fetches[0]!.input.settings).toBeNull();
expect(fetches[1]!.input.settings).toEqual({ model: "gpt-5.6-sol" });
fetches[1]!.resolve(
options({ effectiveSettings: { model: "gpt-5.6-sol" } })
);
await settled();
expect(core.getSnapshot().resolvedSettings.model).toBe("gpt-5.6-sol");
// Stale slow response lands last — must change nothing.
fetches[0]!.resolve(
options({ effectiveSettings: { model: "gpt-5.6-terra" } })
);
await settled();
expect(core.getSnapshot().resolvedSettings.model).toBe("gpt-5.6-sol");
expect(core.getSnapshot().refreshing).toBe(false);
});
it("keeps last good options and reports degraded on refresh failure", async () => {
const { core, fetches } = createHarness();
core.refresh();
fetches[0]!.resolve(options());
await settled();
expect(core.getSnapshot().options).not.toBeNull();
core.refresh();
fetches[1]!.reject(new Error("catalog probe timed out"));
await settled();
const snapshot = core.getSnapshot();
expect(snapshot.options).not.toBeNull();
expect(snapshot.degraded).toBe(true);
expect(snapshot.errorMessage).toBe("catalog probe timed out");
core.refresh();
fetches[2]!.resolve(options());
await settled();
expect(core.getSnapshot().degraded).toBe(false);
});
it("resolves display values from draft over effective settings", async () => {
const { core, fetches } = createHarness();
core.refresh();
fetches[0]!.resolve(options());
await settled();
// No draft: effective settings win.
expect(core.getSnapshot().resolvedSettings).toEqual({
model: "gpt-5.6-terra",
reasoningEffort: "high"
});
core.setSettings({ model: "gpt-5.6-sol" });
expect(core.getSnapshot().resolvedSettings).toEqual({
model: "gpt-5.6-sol",
reasoningEffort: "high"
});
expect(core.resolveSubmitSettings()).toEqual(
core.getSnapshot().resolvedSettings
);
});
it("resets draft and options on target change but keeps draft on cwd change", async () => {
const { core, fetches } = createHarness();
core.refresh();
fetches[0]!.resolve(options());
await settled();
core.setSettings({ model: "gpt-5.6-sol" });
core.setContext({ agentTargetId: "local:codex", cwd: "/other" });
expect(core.getSnapshot().draft).toEqual({ model: "gpt-5.6-sol" });
expect(core.getSnapshot().options).not.toBeNull();
core.setContext({ agentTargetId: "local:claude-code", cwd: "/other" });
const snapshot = core.getSnapshot();
expect(snapshot.draft).toEqual({});
expect(snapshot.options).toBeNull();
expect(snapshot.initialLoading).toBe(true);
// The pending fetch for the old target must not seed the new target.
fetches[fetches.length - 2]!.resolve(
options({ effectiveSettings: { model: "gpt-5.6-sol" } })
);
await settled();
expect(core.getSnapshot().options).toBeNull();
});
it("coalesces defaults writes to a single in-flight trailing patch", async () => {
const writes: Array<{ id: string; patch: ComposerSettingsDraft }> = [];
let release: (() => void) | null = null;
const rememberDefaults = vi.fn(
(id: string, patch: ComposerSettingsDraft) =>
new Promise<void>((resolve) => {
writes.push({ id, patch });
release = resolve;
})
);
const { core } = createHarness(rememberDefaults);
core.setSettings({ model: "gpt-5.6-sol" });
core.setSettings({ reasoningEffort: "high" });
core.setSettings({ model: "gpt-5.6-terra" });
await settled();
// First write in flight with the first patch; later picks coalesced.
expect(writes).toHaveLength(1);
release!();
await settled();
expect(writes).toHaveLength(2);
expect(writes[1]).toEqual({
id: "local:codex",
patch: { model: "gpt-5.6-terra", reasoningEffort: "high" }
});
release!();
await settled();
expect(writes).toHaveLength(2);
});
it("drops pending defaults for the previous target on target change", async () => {
const writes: Array<{ id: string; patch: ComposerSettingsDraft }> = [];
let release: (() => void) | null = null;
const rememberDefaults = (id: string, patch: ComposerSettingsDraft) =>
new Promise<void>((resolve) => {
writes.push({ id, patch });
release = resolve;
});
const { core } = createHarness(rememberDefaults);
core.setSettings({ model: "gpt-5.6-sol" });
core.setSettings({ speed: "fast" }); // pending behind the in-flight write
core.setContext({ agentTargetId: "local:claude-code", cwd: "/repo" });
release!();
await settled();
expect(writes).toHaveLength(1);
expect(writes[0]!.id).toBe("local:codex");
});
it("keeps snapshot reference stable when nothing changes", () => {
const { core } = createHarness();
const first = core.getSnapshot();
expect(core.getSnapshot()).toBe(first);
core.setContext({ agentTargetId: "local:codex", cwd: "/repo" });
expect(core.getSnapshot()).toBe(first);
});
});