-
Notifications
You must be signed in to change notification settings - Fork 15
feat(agent): expose agent version to UI for capability gating #2031
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,34 @@ | ||
| import { isAgentVersion } from "@utils/agentVersion"; | ||
| import { useSessionStore } from "../stores/sessionStore"; | ||
|
|
||
| /** | ||
| * Returns the connected agent's version for the given task, or `undefined` | ||
| * if no session is active or the agent hasn't reported a version yet. | ||
| */ | ||
| export function useAgentVersion( | ||
| taskId: string | undefined, | ||
| ): string | undefined { | ||
| return useSessionStore((s) => { | ||
| if (!taskId) return undefined; | ||
| const taskRunId = s.taskIdIndex[taskId]; | ||
| if (!taskRunId) return undefined; | ||
| return s.sessions[taskRunId]?.agentVersion; | ||
| }); | ||
| } | ||
|
|
||
| /** | ||
| * Returns true when the connected agent's version satisfies the given semver | ||
| * range. Fails closed when the version is unknown — feature gates stay off. | ||
| * | ||
| * Examples: | ||
| * useIsAgentVersion(taskId, ">=0.40.1") | ||
| * useIsAgentVersion(taskId, ">1.0.0") | ||
| * useIsAgentVersion(taskId, ">=0.40.0 <1.0.0") | ||
| */ | ||
| export function useIsAgentVersion( | ||
| taskId: string | undefined, | ||
| range: string, | ||
| ): boolean { | ||
| const version = useAgentVersion(taskId); | ||
| return isAgentVersion(version, range); | ||
| } |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,73 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { describe, expect, it } from "vitest"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { isAgentVersion } from "./agentVersion"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| describe("isAgentVersion", () => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| it("returns true when actual satisfies a >= range", () => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| expect(isAgentVersion("0.40.1", ">=0.40.1")).toBe(true); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| expect(isAgentVersion("0.41.0", ">=0.40.1")).toBe(true); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| expect(isAgentVersion("1.0.0", ">=0.40.1")).toBe(true); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| it("returns false when actual is below a >= range", () => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| expect(isAgentVersion("0.40.0", ">=0.40.1")).toBe(false); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| expect(isAgentVersion("0.39.99", ">=0.40.1")).toBe(false); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| it("supports strict >, <, and <= comparators", () => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| expect(isAgentVersion("1.0.1", ">1.0.0")).toBe(true); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| expect(isAgentVersion("1.0.0", ">1.0.0")).toBe(false); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| expect(isAgentVersion("0.9.9", "<1.0.0")).toBe(true); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| expect(isAgentVersion("1.0.0", "<1.0.0")).toBe(false); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| expect(isAgentVersion("1.0.0", "<=1.0.0")).toBe(true); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| it("supports caret and tilde ranges", () => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| expect(isAgentVersion("1.2.5", "^1.2.0")).toBe(true); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| expect(isAgentVersion("2.0.0", "^1.2.0")).toBe(false); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| expect(isAgentVersion("1.2.5", "~1.2.0")).toBe(true); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| expect(isAgentVersion("1.3.0", "~1.2.0")).toBe(false); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| it("supports compound ranges", () => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| expect(isAgentVersion("0.50.0", ">=0.40.0 <1.0.0")).toBe(true); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| expect(isAgentVersion("1.0.0", ">=0.40.0 <1.0.0")).toBe(false); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| expect(isAgentVersion("0.39.0", ">=0.40.0 <1.0.0")).toBe(false); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+5
to
+35
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
Prompt To Fix With AIThis is a comment left during a code review.
Path: apps/code/src/renderer/utils/agentVersion.test.ts
Line: 5-35
Comment:
These test blocks contain multiple inline assertions for clearly data-driven inputs, which the team's style guide flags as a preference for parameterised tests (`it.each`). If the first assertion fails, subsequent ones never run, making debugging harder. Consider refactoring into `it.each` tables.
```suggestion
it.each([
["0.40.1", ">=0.40.1", true],
["0.41.0", ">=0.40.1", true],
["1.0.0", ">=0.40.1", true],
["0.40.0", ">=0.40.1", false],
["0.39.99", ">=0.40.1", false],
["1.0.1", ">1.0.0", true],
["1.0.0", ">1.0.0", false],
["0.9.9", "<1.0.0", true],
["1.0.0", "<1.0.0", false],
["1.0.0", "<=1.0.0", true],
["1.2.5", "^1.2.0", true],
["2.0.0", "^1.2.0", false],
["1.2.5", "~1.2.0", true],
["1.3.0", "~1.2.0", false],
["0.50.0", ">=0.40.0 <1.0.0", true],
["1.0.0", ">=0.40.0 <1.0.0", false],
["0.39.0", ">=0.40.0 <1.0.0", false],
])(
"isAgentVersion(%s, %s) === %s",
(version, range, expected) => {
expect(isAgentVersion(version, range)).toBe(expected);
},
);
```
How can I resolve this? If you propose a fix, please make it concise.Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time! |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| it("treats prereleases as comparable to their base version", () => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // includePrerelease lets call sites match a prerelease against a stable | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // range without callers having to opt in per call. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| expect(isAgentVersion("0.40.1-rc.1", ">=0.40.0")).toBe(true); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| expect(isAgentVersion("0.40.0-rc.1", ">=0.40.1")).toBe(false); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| it("fails closed when the actual version is undefined", () => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| expect(isAgentVersion(undefined, ">=0.0.0")).toBe(false); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| expect(isAgentVersion(undefined, "<99.0.0")).toBe(false); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| it("fails closed when the actual version is empty", () => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| expect(isAgentVersion("", ">=0.0.0")).toBe(false); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| it("returns false for malformed range strings", () => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| expect(isAgentVersion("1.0.0", "not a range")).toBe(false); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| describe("dev sentinel (0.0.0-dev)", () => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Local dev builds ship the latest code under the placeholder version | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // `0.0.0-dev`. Treat it as satisfying any range so feature gates don't | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // silently disable in development. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| it("satisfies any well-formed range", () => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| expect(isAgentVersion("0.0.0-dev", ">=0.40.1")).toBe(true); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| expect(isAgentVersion("0.0.0-dev", ">99.0.0")).toBe(true); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| expect(isAgentVersion("0.0.0-dev", "<0.0.0")).toBe(true); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| expect(isAgentVersion("0.0.0-dev", "^1.2.0")).toBe(true); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| expect(isAgentVersion("0.0.0-dev", ">=0.40.0 <1.0.0")).toBe(true); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| it("still rejects malformed range strings", () => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| expect(isAgentVersion("0.0.0-dev", "not a range")).toBe(false); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,29 @@ | ||
| import semver from "semver"; | ||
|
|
||
| /** Sentinel version used by unbuilt dev builds (matches the placeholder in | ||
| * `packages/agent/package.json`). Real release builds inject a real semver. */ | ||
| const DEV_VERSION = "0.0.0-dev"; | ||
|
|
||
| /** | ||
| * Check whether the connected agent's version satisfies a semver range. | ||
| * | ||
| * Examples: | ||
| * isAgentVersion(version, ">=0.40.1") | ||
| * isAgentVersion(version, ">1.0.0") | ||
| * isAgentVersion(version, ">=0.40.0 <1.0.0") | ||
| * | ||
| * Returns `false` when the agent version is unknown so feature gates fail | ||
| * closed — an unknown agent never accidentally enables a newer code path. | ||
| * | ||
| * The dev sentinel `0.0.0-dev` is treated as "satisfies any range": local | ||
| * dev builds carry the latest code, so we want feature gates to open even | ||
| * though the literal semver is below every released version. | ||
| */ | ||
| export function isAgentVersion( | ||
| actual: string | undefined, | ||
| range: string, | ||
| ): boolean { | ||
| if (!actual) return false; | ||
| if (actual === DEV_VERSION) return semver.validRange(range) !== null; | ||
| return semver.satisfies(actual, range, { includePrerelease: true }); | ||
| } |
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
agentVersioncaptureThe PR description explicitly states that version capture is unconditional — it applies to both cloud and local sessions. The new test only exercises the cloud path (
isCloud: true). A complementary test for a local (non-cloud) session verifying thatagentVersionis stored — and thatstatusis NOT flipped to"connected"— would confirm the unconditional behaviour described in the PR and guard against an accidentalisCloudgate being added in future.Prompt To Fix With AI