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
382 lines
11 KiB
TypeScript
382 lines
11 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
import {
|
|
buildAgentEnvWizardViewModel,
|
|
deriveHasAnomaly,
|
|
type AgentEnvWizardViewModelInput
|
|
} from "./agentEnvViewModel";
|
|
import type { AgentProviderStatus } from "@tutti-os/client-tuttid-ts";
|
|
|
|
const LABELS = {
|
|
detect: "检测",
|
|
network: "网络",
|
|
install: "安装",
|
|
adapter: "适配器",
|
|
login: "登录",
|
|
ready: "就绪"
|
|
};
|
|
|
|
function status(
|
|
overrides: Partial<AgentProviderStatus> = {}
|
|
): AgentProviderStatus {
|
|
return {
|
|
provider: "codex",
|
|
availability: { status: "ready", reasonCode: null },
|
|
cli: {
|
|
installed: true,
|
|
binaryPath: "/usr/bin/codex",
|
|
version: "1.2.3",
|
|
minVersion: "1.0.0"
|
|
},
|
|
adapter: {
|
|
installed: true,
|
|
binaryPath: "/opt/acp",
|
|
command: ["acp"],
|
|
version: "2.0.0",
|
|
requiredVersion: "2.0.0"
|
|
},
|
|
auth: {
|
|
status: "authenticated",
|
|
accountLabel: "me@x.com",
|
|
authMethod: "oauth"
|
|
},
|
|
actions: [],
|
|
network: null,
|
|
activeAction: null,
|
|
...overrides
|
|
} as AgentProviderStatus;
|
|
}
|
|
|
|
function input(
|
|
overrides: Partial<AgentEnvWizardViewModelInput> = {}
|
|
): AgentEnvWizardViewModelInput {
|
|
return {
|
|
provider: "codex",
|
|
status: status(),
|
|
isLoading: false,
|
|
activeAction: null,
|
|
installActionPending: false,
|
|
updateActionPending: false,
|
|
loginPending: false,
|
|
revealIndex: Number.MAX_SAFE_INTEGER,
|
|
stageLabels: LABELS,
|
|
...overrides
|
|
};
|
|
}
|
|
|
|
describe("buildAgentEnvWizardViewModel", () => {
|
|
it("marks ready and all stages ok for a fully-configured provider", () => {
|
|
const vm = buildAgentEnvWizardViewModel(input());
|
|
expect(vm.ready).toBe(true);
|
|
expect(vm.displayStages.every((s) => s.status === "ok")).toBe(true);
|
|
expect(vm.blockingStageId).toBeNull();
|
|
});
|
|
|
|
it("keeps configured credentials usable without claiming remote authentication", () => {
|
|
const vm = buildAgentEnvWizardViewModel(
|
|
input({
|
|
status: status({
|
|
auth: { status: "configured", accountLabel: null }
|
|
})
|
|
})
|
|
);
|
|
|
|
expect(vm.ready).toBe(true);
|
|
expect(vm.displayStages.find((stage) => stage.id === "login")?.status).toBe(
|
|
"ok"
|
|
);
|
|
expect(vm.blockingStageId).toBeNull();
|
|
});
|
|
|
|
it("omits the account detail when authentication has no account label", () => {
|
|
const vm = buildAgentEnvWizardViewModel(
|
|
input({
|
|
status: status({
|
|
auth: { status: "authenticated", accountLabel: null }
|
|
})
|
|
})
|
|
);
|
|
|
|
expect(
|
|
vm.displayStages.find((stage) => stage.id === "login")?.detail
|
|
).toBeNull();
|
|
});
|
|
|
|
it("projects a discovered update and keeps the CLI stage busy while it runs", () => {
|
|
const updateStatus = status({
|
|
actions: [{ id: "update", kind: "daemon_action" }],
|
|
update: {
|
|
capability: "supported",
|
|
source: "npm",
|
|
currentVersion: "1.2.3",
|
|
latestVersion: "1.3.0",
|
|
updateAvailable: true,
|
|
unsupportedReason: null,
|
|
lastCheckedAt: "2026-07-19T00:00:00Z",
|
|
reasonCode: null
|
|
}
|
|
});
|
|
const available = buildAgentEnvWizardViewModel(
|
|
input({ status: updateStatus })
|
|
);
|
|
expect(available.updateAvailable).toBe(true);
|
|
|
|
const pending = buildAgentEnvWizardViewModel(
|
|
input({ status: updateStatus, updateActionPending: true })
|
|
);
|
|
expect(pending.busy).toBe(true);
|
|
expect(pending.updating).toBe(true);
|
|
expect(
|
|
pending.displayStages.find((stage) => stage.id === "install")?.status
|
|
).toBe("running");
|
|
});
|
|
|
|
it("shows the version-floor token when the CLI is below the supported floor", () => {
|
|
const vm = buildAgentEnvWizardViewModel(
|
|
input({
|
|
status: status({
|
|
availability: {
|
|
status: "unsupported",
|
|
reasonCode: "codex_version_too_old"
|
|
},
|
|
cli: {
|
|
installed: true,
|
|
version: "0.9.0",
|
|
minVersion: "1.0.0",
|
|
binaryPath: "/usr/bin/codex"
|
|
}
|
|
}),
|
|
revealIndex: Number.MAX_SAFE_INTEGER
|
|
})
|
|
);
|
|
const install = vm.displayStages.find((s) => s.id === "install");
|
|
expect(install?.detail).toEqual({
|
|
kind: "version-floor",
|
|
current: "0.9.0",
|
|
required: "1.0.0"
|
|
});
|
|
expect(install?.status).toBe("error");
|
|
});
|
|
|
|
it("shows the version-floor token for a generic provider version gate", () => {
|
|
const vm = buildAgentEnvWizardViewModel(
|
|
input({
|
|
status: status({
|
|
availability: {
|
|
status: "not_installed",
|
|
reasonCode: "cli_version_unsupported"
|
|
},
|
|
cli: {
|
|
installed: true,
|
|
version: "0.0.2",
|
|
minVersion: "0.0.4",
|
|
binaryPath: "/usr/bin/tutti-agent"
|
|
}
|
|
}),
|
|
revealIndex: Number.MAX_SAFE_INTEGER
|
|
})
|
|
);
|
|
const install = vm.displayStages.find((s) => s.id === "install");
|
|
expect(install?.detail).toEqual({
|
|
kind: "version-floor",
|
|
current: "0.0.2",
|
|
required: "0.0.4"
|
|
});
|
|
expect(install?.status).toBe("error");
|
|
});
|
|
|
|
it("does NOT red the install (CLI) stage on an adapter version mismatch", () => {
|
|
const vm = buildAgentEnvWizardViewModel(
|
|
input({
|
|
status: status({
|
|
availability: {
|
|
status: "unsupported",
|
|
reasonCode: "acp_adapter_version_mismatch"
|
|
},
|
|
adapter: {
|
|
installed: true,
|
|
version: "1.0.0",
|
|
requiredVersion: "2.0.0",
|
|
command: ["acp"],
|
|
binaryPath: "/opt/acp"
|
|
}
|
|
})
|
|
})
|
|
);
|
|
expect(vm.displayStages.find((s) => s.id === "install")?.status).toBe("ok");
|
|
const adapter = vm.displayStages.find((s) => s.id === "adapter");
|
|
expect(adapter?.status).toBe("error");
|
|
expect(adapter?.detail).toEqual({
|
|
kind: "version-mismatch",
|
|
current: "1.0.0",
|
|
required: "2.0.0"
|
|
});
|
|
});
|
|
|
|
it("shows installed and required Tutti Agent adapter versions on mismatch", () => {
|
|
const vm = buildAgentEnvWizardViewModel(
|
|
input({
|
|
provider: "tutti-agent",
|
|
status: status({
|
|
provider: "tutti-agent",
|
|
availability: {
|
|
status: "not_installed",
|
|
reasonCode: "acp_adapter_version_mismatch"
|
|
},
|
|
adapter: {
|
|
installed: false,
|
|
version: "0.0.1",
|
|
requiredVersion: "0.0.3",
|
|
command: ["tutti-agent", "app-server"],
|
|
binaryPath: "/opt/tutti-agent"
|
|
}
|
|
})
|
|
})
|
|
);
|
|
const adapter = vm.displayStages.find((s) => s.id === "adapter");
|
|
expect(adapter?.status).toBe("error");
|
|
expect(adapter?.detail).toEqual({
|
|
kind: "version-mismatch",
|
|
current: "0.0.1",
|
|
required: "0.0.3"
|
|
});
|
|
});
|
|
|
|
it("assembles network checks and treats an unconfigured proxy as reachable", () => {
|
|
const vm = buildAgentEnvWizardViewModel(
|
|
input({
|
|
status: status({
|
|
network: {
|
|
registry: {
|
|
reachable: true,
|
|
endpoint: "https://registry.npmjs.org"
|
|
},
|
|
providerApi: {
|
|
reachable: false,
|
|
endpoint: "https://api.openai.com/v1"
|
|
},
|
|
proxy: { configured: false, reachable: false, url: null }
|
|
}
|
|
})
|
|
})
|
|
);
|
|
expect(vm.networkChecks).toEqual([
|
|
{ kind: "registry", reachable: true, host: "registry.npmjs.org" },
|
|
{ kind: "api", reachable: false, host: "api.openai.com" },
|
|
{ kind: "proxy", reachable: true, host: null, configured: false }
|
|
]);
|
|
});
|
|
|
|
it("strips credentials from a configured proxy URL", () => {
|
|
const vm = buildAgentEnvWizardViewModel(
|
|
input({
|
|
status: status({
|
|
network: {
|
|
registry: {
|
|
reachable: true,
|
|
endpoint: "https://registry.npmjs.org"
|
|
},
|
|
proxy: {
|
|
configured: true,
|
|
reachable: true,
|
|
url: "http://user:s3cr3t@proxy.corp:8080"
|
|
}
|
|
}
|
|
})
|
|
})
|
|
);
|
|
const proxy = vm.networkChecks.find((c) => c.kind === "proxy");
|
|
expect(proxy?.host).toBe("http://proxy.corp:8080");
|
|
expect(proxy?.host).not.toContain("s3cr3t");
|
|
});
|
|
|
|
it("surfaces the first non-ok stage as blocking once revealed", () => {
|
|
const vm = buildAgentEnvWizardViewModel(
|
|
input({
|
|
status: status({
|
|
availability: { status: "auth_required", reasonCode: null },
|
|
auth: { status: "required", accountLabel: null, authMethod: null }
|
|
})
|
|
})
|
|
);
|
|
expect(vm.blockingStageId).toBe("login");
|
|
});
|
|
|
|
it("hides the blocking stage when the reveal cursor has not reached it", () => {
|
|
const vm = buildAgentEnvWizardViewModel(
|
|
input({
|
|
status: status({
|
|
auth: { status: "required", accountLabel: null, authMethod: null },
|
|
availability: { status: "auth_required", reasonCode: null }
|
|
}),
|
|
revealIndex: 0
|
|
})
|
|
);
|
|
expect(vm.blockingStageId).toBeNull();
|
|
});
|
|
|
|
it("exposes the manual install command for codex", () => {
|
|
expect(buildAgentEnvWizardViewModel(input()).manualCommand).toBe(
|
|
"npm install -g @openai/codex --include=optional"
|
|
);
|
|
});
|
|
|
|
it("exposes the daemon-matching manual install command for claude code", () => {
|
|
expect(
|
|
buildAgentEnvWizardViewModel(input({ provider: "claude-code" }))
|
|
.manualCommand
|
|
).toBe("curl -fsSL https://claude.ai/install.sh | bash");
|
|
});
|
|
|
|
it("flags the install stage pending with a platform-incomplete problem when the launcher is present but the platform subpackage is missing", () => {
|
|
const vm = buildAgentEnvWizardViewModel(
|
|
input({
|
|
status: status({
|
|
availability: {
|
|
status: "not_installed",
|
|
reasonCode: "codex_platform_pkg_incomplete"
|
|
},
|
|
// The launcher itself is resolved, so without the platform-incomplete
|
|
// signal the install stage would wrongly read as ok.
|
|
cli: {
|
|
installed: true,
|
|
version: "1.2.3",
|
|
minVersion: "1.0.0",
|
|
binaryPath: "/usr/bin/codex"
|
|
}
|
|
})
|
|
})
|
|
);
|
|
const install = vm.displayStages.find((s) => s.id === "install");
|
|
expect(install?.status).toBe("pending");
|
|
expect(install?.problem).toBe("install-platform-incomplete");
|
|
expect(vm.ready).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe("deriveHasAnomaly", () => {
|
|
it("is true when any stage is in error", () => {
|
|
expect(
|
|
deriveHasAnomaly(
|
|
[{ id: "adapter", label: "x", status: "error", detail: null }],
|
|
null
|
|
)
|
|
).toBe(true);
|
|
});
|
|
it("is true when the active action carries an error", () => {
|
|
expect(
|
|
deriveHasAnomaly(
|
|
[{ id: "ready", label: "x", status: "ok", detail: null }],
|
|
{ code: "boom", message: "failed" }
|
|
)
|
|
).toBe(true);
|
|
});
|
|
it("is false for a clean not-yet-set-up flow", () => {
|
|
expect(
|
|
deriveHasAnomaly(
|
|
[{ id: "login", label: "x", status: "pending", detail: null }],
|
|
null
|
|
)
|
|
).toBe(false);
|
|
});
|
|
});
|