@kenkaiiii/ggcoder 4.3.205 → 4.3.207
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/dist/cli.js +38 -26
- package/dist/cli.js.map +1 -1
- package/dist/config.js +2 -2
- package/dist/core/compaction/compactor.d.ts.map +1 -1
- package/dist/core/compaction/compactor.js +6 -0
- package/dist/core/compaction/compactor.js.map +1 -1
- package/dist/core/compaction/compactor.test.js +16 -0
- package/dist/core/compaction/compactor.test.js.map +1 -1
- package/dist/core/goal-controller.d.ts +57 -0
- package/dist/core/goal-controller.d.ts.map +1 -0
- package/dist/core/goal-controller.js +285 -0
- package/dist/core/goal-controller.js.map +1 -0
- package/dist/core/goal-controller.test.d.ts +2 -0
- package/dist/core/goal-controller.test.d.ts.map +1 -0
- package/dist/core/goal-controller.test.js +419 -0
- package/dist/core/goal-controller.test.js.map +1 -0
- package/dist/core/goal-lifecycle-smoke.test.d.ts +2 -0
- package/dist/core/goal-lifecycle-smoke.test.d.ts.map +1 -0
- package/dist/core/goal-lifecycle-smoke.test.js +207 -0
- package/dist/core/goal-lifecycle-smoke.test.js.map +1 -0
- package/dist/core/goal-store.d.ts +164 -0
- package/dist/core/goal-store.d.ts.map +1 -0
- package/dist/core/goal-store.js +721 -0
- package/dist/core/goal-store.js.map +1 -0
- package/dist/core/goal-store.test.d.ts +2 -0
- package/dist/core/goal-store.test.d.ts.map +1 -0
- package/dist/core/goal-store.test.js +341 -0
- package/dist/core/goal-store.test.js.map +1 -0
- package/dist/core/goal-verifier.d.ts +17 -0
- package/dist/core/goal-verifier.d.ts.map +1 -0
- package/dist/core/goal-verifier.js +84 -0
- package/dist/core/goal-verifier.js.map +1 -0
- package/dist/core/goal-verifier.test.d.ts +2 -0
- package/dist/core/goal-verifier.test.d.ts.map +1 -0
- package/dist/core/goal-verifier.test.js +88 -0
- package/dist/core/goal-verifier.test.js.map +1 -0
- package/dist/core/goal-worker-dev-server-lifecycle.test.d.ts +2 -0
- package/dist/core/goal-worker-dev-server-lifecycle.test.d.ts.map +1 -0
- package/dist/core/goal-worker-dev-server-lifecycle.test.js +68 -0
- package/dist/core/goal-worker-dev-server-lifecycle.test.js.map +1 -0
- package/dist/core/goal-worker.d.ts +51 -0
- package/dist/core/goal-worker.d.ts.map +1 -0
- package/dist/core/goal-worker.js +339 -0
- package/dist/core/goal-worker.js.map +1 -0
- package/dist/core/goal-worker.test.d.ts +2 -0
- package/dist/core/goal-worker.test.d.ts.map +1 -0
- package/dist/core/goal-worker.test.js +224 -0
- package/dist/core/goal-worker.test.js.map +1 -0
- package/dist/core/model-registry.test.js +51 -1
- package/dist/core/model-registry.test.js.map +1 -1
- package/dist/core/oauth/gemini.d.ts.map +1 -1
- package/dist/core/oauth/gemini.js +138 -30
- package/dist/core/oauth/gemini.js.map +1 -1
- package/dist/core/oauth/gemini.test.d.ts +2 -0
- package/dist/core/oauth/gemini.test.d.ts.map +1 -0
- package/dist/core/oauth/gemini.test.js +154 -0
- package/dist/core/oauth/gemini.test.js.map +1 -0
- package/dist/core/process-manager-dev-server-repro.test.d.ts +2 -0
- package/dist/core/process-manager-dev-server-repro.test.d.ts.map +1 -0
- package/dist/core/process-manager-dev-server-repro.test.js +100 -0
- package/dist/core/process-manager-dev-server-repro.test.js.map +1 -0
- package/dist/core/process-manager.js +2 -2
- package/dist/core/process-manager.js.map +1 -1
- package/dist/core/prompt-commands.d.ts.map +1 -1
- package/dist/core/prompt-commands.js +125 -0
- package/dist/core/prompt-commands.js.map +1 -1
- package/dist/core/prompt-commands.test.js +38 -0
- package/dist/core/prompt-commands.test.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/interactive.d.ts.map +1 -1
- package/dist/interactive.js +20 -11
- package/dist/interactive.js.map +1 -1
- package/dist/system-prompt.d.ts.map +1 -1
- package/dist/system-prompt.js +19 -50
- package/dist/system-prompt.js.map +1 -1
- package/dist/system-prompt.test.js +124 -1
- package/dist/system-prompt.test.js.map +1 -1
- package/dist/tools/edit-diff.d.ts.map +1 -1
- package/dist/tools/edit-diff.js +71 -32
- package/dist/tools/edit-diff.js.map +1 -1
- package/dist/tools/edit-diff.test.js +14 -0
- package/dist/tools/edit-diff.test.js.map +1 -1
- package/dist/tools/edit.d.ts.map +1 -1
- package/dist/tools/edit.js +38 -18
- package/dist/tools/edit.js.map +1 -1
- package/dist/tools/edit.test.js +56 -6
- package/dist/tools/edit.test.js.map +1 -1
- package/dist/tools/enter-plan.d.ts.map +1 -1
- package/dist/tools/enter-plan.js +1 -0
- package/dist/tools/enter-plan.js.map +1 -1
- package/dist/tools/goals.d.ts +110 -0
- package/dist/tools/goals.d.ts.map +1 -0
- package/dist/tools/goals.js +500 -0
- package/dist/tools/goals.js.map +1 -0
- package/dist/tools/goals.test.d.ts +2 -0
- package/dist/tools/goals.test.d.ts.map +1 -0
- package/dist/tools/goals.test.js +431 -0
- package/dist/tools/goals.test.js.map +1 -0
- package/dist/tools/index.d.ts +2 -0
- package/dist/tools/index.d.ts.map +1 -1
- package/dist/tools/index.js +6 -0
- package/dist/tools/index.js.map +1 -1
- package/dist/tools/prompt-hints.d.ts.map +1 -1
- package/dist/tools/prompt-hints.js +2 -0
- package/dist/tools/prompt-hints.js.map +1 -1
- package/dist/tools/source-path.d.ts +9 -0
- package/dist/tools/source-path.d.ts.map +1 -0
- package/dist/tools/source-path.js +119 -0
- package/dist/tools/source-path.js.map +1 -0
- package/dist/tools/source-path.test.d.ts +2 -0
- package/dist/tools/source-path.test.d.ts.map +1 -0
- package/dist/tools/source-path.test.js +80 -0
- package/dist/tools/source-path.test.js.map +1 -0
- package/dist/tools/subagent.js +16 -0
- package/dist/tools/subagent.js.map +1 -1
- package/dist/ui/App.d.ts +73 -4
- package/dist/ui/App.d.ts.map +1 -1
- package/dist/ui/App.js +1068 -140
- package/dist/ui/App.js.map +1 -1
- package/dist/ui/activity-phrases.d.ts.map +1 -1
- package/dist/ui/activity-phrases.js +7 -3
- package/dist/ui/activity-phrases.js.map +1 -1
- package/dist/ui/app-state-persistence.test.d.ts +2 -0
- package/dist/ui/app-state-persistence.test.d.ts.map +1 -0
- package/dist/ui/app-state-persistence.test.js +130 -0
- package/dist/ui/app-state-persistence.test.js.map +1 -0
- package/dist/ui/components/BackgroundTasksBar.d.ts +16 -1
- package/dist/ui/components/BackgroundTasksBar.d.ts.map +1 -1
- package/dist/ui/components/BackgroundTasksBar.js +15 -2
- package/dist/ui/components/BackgroundTasksBar.js.map +1 -1
- package/dist/ui/components/Banner.d.ts +2 -1
- package/dist/ui/components/Banner.d.ts.map +1 -1
- package/dist/ui/components/Banner.js +3 -3
- package/dist/ui/components/Banner.js.map +1 -1
- package/dist/ui/components/GoalOverlay.d.ts +74 -0
- package/dist/ui/components/GoalOverlay.d.ts.map +1 -0
- package/dist/ui/components/GoalOverlay.js +675 -0
- package/dist/ui/components/GoalOverlay.js.map +1 -0
- package/dist/ui/components/GoalStatusBar.d.ts +24 -0
- package/dist/ui/components/GoalStatusBar.d.ts.map +1 -0
- package/dist/ui/components/GoalStatusBar.js +113 -0
- package/dist/ui/components/GoalStatusBar.js.map +1 -0
- package/dist/ui/components/InputArea.d.ts +2 -1
- package/dist/ui/components/InputArea.d.ts.map +1 -1
- package/dist/ui/components/InputArea.js +44 -2
- package/dist/ui/components/InputArea.js.map +1 -1
- package/dist/ui/components/InputArea.test.d.ts +2 -0
- package/dist/ui/components/InputArea.test.d.ts.map +1 -0
- package/dist/ui/components/InputArea.test.js +79 -0
- package/dist/ui/components/InputArea.test.js.map +1 -0
- package/dist/ui/components/ToolExecution.d.ts.map +1 -1
- package/dist/ui/components/ToolExecution.js +96 -3
- package/dist/ui/components/ToolExecution.js.map +1 -1
- package/dist/ui/footer-status-layout.test.d.ts +2 -0
- package/dist/ui/footer-status-layout.test.d.ts.map +1 -0
- package/dist/ui/footer-status-layout.test.js +56 -0
- package/dist/ui/footer-status-layout.test.js.map +1 -0
- package/dist/ui/goal-events.d.ts +107 -0
- package/dist/ui/goal-events.d.ts.map +1 -0
- package/dist/ui/goal-events.js +323 -0
- package/dist/ui/goal-events.js.map +1 -0
- package/dist/ui/goal-events.test.d.ts +2 -0
- package/dist/ui/goal-events.test.d.ts.map +1 -0
- package/dist/ui/goal-events.test.js +293 -0
- package/dist/ui/goal-events.test.js.map +1 -0
- package/dist/ui/goal-overlay.test.d.ts +2 -0
- package/dist/ui/goal-overlay.test.d.ts.map +1 -0
- package/dist/ui/goal-overlay.test.js +276 -0
- package/dist/ui/goal-overlay.test.js.map +1 -0
- package/dist/ui/goal-status-bar.test.d.ts +2 -0
- package/dist/ui/goal-status-bar.test.d.ts.map +1 -0
- package/dist/ui/goal-status-bar.test.js +143 -0
- package/dist/ui/goal-status-bar.test.js.map +1 -0
- package/dist/ui/live-item-flush.test.js +48 -0
- package/dist/ui/live-item-flush.test.js.map +1 -1
- package/dist/ui/render.d.ts +11 -4
- package/dist/ui/render.d.ts.map +1 -1
- package/dist/ui/render.js +12 -3
- package/dist/ui/render.js.map +1 -1
- package/dist/ui/scroll-stabilization.test.d.ts +2 -0
- package/dist/ui/scroll-stabilization.test.d.ts.map +1 -0
- package/dist/ui/scroll-stabilization.test.js +70 -0
- package/dist/ui/scroll-stabilization.test.js.map +1 -0
- package/dist/ui/slash-command-images.test.d.ts +2 -0
- package/dist/ui/slash-command-images.test.d.ts.map +1 -0
- package/dist/ui/slash-command-images.test.js +47 -0
- package/dist/ui/slash-command-images.test.js.map +1 -0
- package/dist/utils/format.js +44 -0
- package/dist/utils/format.js.map +1 -1
- package/package.json +6 -5
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
import { EventEmitter } from "node:events";
|
|
3
|
+
import { PassThrough } from "node:stream";
|
|
4
|
+
import fs from "node:fs/promises";
|
|
5
|
+
import os from "node:os";
|
|
6
|
+
import path from "node:path";
|
|
7
|
+
import { getGoalRun, updateGoalTask, upsertGoalRun } from "./goal-store.js";
|
|
8
|
+
const spawnMock = vi.hoisted(() => vi.fn());
|
|
9
|
+
const killProcessTreeMock = vi.hoisted(() => vi.fn());
|
|
10
|
+
vi.mock("node:child_process", () => ({ spawn: spawnMock }));
|
|
11
|
+
vi.mock("../utils/process.js", () => ({ killProcessTree: killProcessTreeMock }));
|
|
12
|
+
class FakeChild extends EventEmitter {
|
|
13
|
+
stdout = new PassThrough();
|
|
14
|
+
stderr = new PassThrough();
|
|
15
|
+
pid = 4242;
|
|
16
|
+
kill = vi.fn();
|
|
17
|
+
}
|
|
18
|
+
let tmpBase;
|
|
19
|
+
let tmpProject;
|
|
20
|
+
let child;
|
|
21
|
+
async function flushUntil(assertion) {
|
|
22
|
+
let lastError;
|
|
23
|
+
for (let i = 0; i < 50; i += 1) {
|
|
24
|
+
await new Promise((resolve) => setTimeout(resolve, 1));
|
|
25
|
+
try {
|
|
26
|
+
await assertion();
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
catch (error) {
|
|
30
|
+
lastError = error;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
throw lastError;
|
|
34
|
+
}
|
|
35
|
+
async function seedGoal() {
|
|
36
|
+
await upsertGoalRun(tmpProject, {
|
|
37
|
+
id: "goal-a",
|
|
38
|
+
title: "Worker harness",
|
|
39
|
+
goal: "Exercise worker exits",
|
|
40
|
+
status: "ready",
|
|
41
|
+
successCriteria: ["worker covered"],
|
|
42
|
+
prerequisites: [],
|
|
43
|
+
harness: [],
|
|
44
|
+
tasks: [],
|
|
45
|
+
evidence: [],
|
|
46
|
+
blockers: [],
|
|
47
|
+
});
|
|
48
|
+
await updateGoalTask(tmpProject, "goal-a", "task-a", {
|
|
49
|
+
id: "task-a",
|
|
50
|
+
title: "Run worker",
|
|
51
|
+
prompt: "Do work",
|
|
52
|
+
status: "pending",
|
|
53
|
+
attempts: 0,
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
async function start(onComplete = vi.fn()) {
|
|
57
|
+
const mod = await import("./goal-worker.js");
|
|
58
|
+
const record = await mod.startGoalWorker({
|
|
59
|
+
cwd: tmpProject,
|
|
60
|
+
provider: "anthropic",
|
|
61
|
+
model: "claude-test",
|
|
62
|
+
goalRunId: "goal-a",
|
|
63
|
+
goalTaskId: "task-a",
|
|
64
|
+
prompt: "Do deterministic work",
|
|
65
|
+
onComplete,
|
|
66
|
+
});
|
|
67
|
+
return { mod, record, onComplete };
|
|
68
|
+
}
|
|
69
|
+
beforeEach(async () => {
|
|
70
|
+
tmpBase = await fs.mkdtemp(path.join(os.tmpdir(), "goal-worker-test-base-"));
|
|
71
|
+
tmpProject = await fs.mkdtemp(path.join(os.tmpdir(), "goal-worker-test-project-"));
|
|
72
|
+
process.env.GG_GOALS_BASE = tmpBase;
|
|
73
|
+
child = new FakeChild();
|
|
74
|
+
spawnMock.mockReturnValue(child);
|
|
75
|
+
killProcessTreeMock.mockReset();
|
|
76
|
+
process.argv[1] = "/tmp/fake-ggcoder-cli.js";
|
|
77
|
+
await seedGoal();
|
|
78
|
+
});
|
|
79
|
+
afterEach(async () => {
|
|
80
|
+
const mod = await import("./goal-worker.js");
|
|
81
|
+
mod.shutdownGoalWorkers(tmpProject);
|
|
82
|
+
vi.clearAllMocks();
|
|
83
|
+
delete process.env.GG_GOALS_BASE;
|
|
84
|
+
await fs.rm(tmpBase, { recursive: true, force: true });
|
|
85
|
+
await fs.rm(tmpProject, { recursive: true, force: true });
|
|
86
|
+
});
|
|
87
|
+
describe("goal worker failure propagation", () => {
|
|
88
|
+
it("prompts workers with explicit durable Goal context before claiming verification", async () => {
|
|
89
|
+
await start();
|
|
90
|
+
const args = spawnMock.mock.calls[0]?.[1];
|
|
91
|
+
const systemPromptIndex = args.indexOf("--system-prompt") + 1;
|
|
92
|
+
const prompt = args[systemPromptIndex];
|
|
93
|
+
expect(prompt).toContain(`cwd=${tmpProject}`);
|
|
94
|
+
expect(prompt).toContain("run_id=goal-a");
|
|
95
|
+
expect(prompt).toContain("task_id=task-a");
|
|
96
|
+
expect(prompt).toContain("create needed scripts/fixtures/harnesses");
|
|
97
|
+
expect(prompt).toContain("source_path/docs/kencode real-code research when relevant");
|
|
98
|
+
expect(prompt).toContain("command/file evidence");
|
|
99
|
+
expect(prompt).toContain('goals({ action: "evidence" | "task", run_id: "goal-a", task_id: "task-a"');
|
|
100
|
+
});
|
|
101
|
+
it("exports a testable worker system prompt/context helper", async () => {
|
|
102
|
+
const mod = await import("./goal-worker.js");
|
|
103
|
+
const prompt = mod.buildGoalWorkerSystemPrompt({
|
|
104
|
+
cwd: "/repo",
|
|
105
|
+
goalRunId: "goal-123",
|
|
106
|
+
goalTaskId: "task-456",
|
|
107
|
+
taskTitle: "Implement typed handoff",
|
|
108
|
+
});
|
|
109
|
+
expect(prompt).toContain("cwd=/repo");
|
|
110
|
+
expect(prompt).toContain("run_id=goal-123");
|
|
111
|
+
expect(prompt).toContain("task_id=task-456");
|
|
112
|
+
expect(prompt).toContain("Implement typed handoff");
|
|
113
|
+
expect(prompt).toContain("Do not mark the whole goal complete");
|
|
114
|
+
});
|
|
115
|
+
it("marks the task done, persists evidence, and notifies callbacks/subscribers for worker success", async () => {
|
|
116
|
+
const { mod, record, onComplete } = await start();
|
|
117
|
+
const listener = vi.fn();
|
|
118
|
+
const unsubscribe = mod.subscribeGoalWorkerCompletions(listener, tmpProject);
|
|
119
|
+
child.stdout.write(JSON.stringify({ type: "text_delta", text: "implemented" }) + "\n");
|
|
120
|
+
child.stdout.write(JSON.stringify({
|
|
121
|
+
type: "tool_call_start",
|
|
122
|
+
toolCallId: "tool-a",
|
|
123
|
+
name: "bash",
|
|
124
|
+
args: { command: "pnpm test" },
|
|
125
|
+
}) + "\n");
|
|
126
|
+
child.stdout.write(JSON.stringify({ type: "tool_call_end", toolCallId: "tool-a", isError: false }) + "\n");
|
|
127
|
+
child.emit("close", 0);
|
|
128
|
+
await flushUntil(() => expect(onComplete).toHaveBeenCalled());
|
|
129
|
+
const run = await getGoalRun(tmpProject, "goal-a");
|
|
130
|
+
expect(record.status).toBe("done");
|
|
131
|
+
expect(run?.activeWorkerId).toBeUndefined();
|
|
132
|
+
expect(run?.tasks[0]).toMatchObject({
|
|
133
|
+
status: "done",
|
|
134
|
+
workerId: record.id,
|
|
135
|
+
lastSummary: expect.stringContaining("implemented"),
|
|
136
|
+
});
|
|
137
|
+
expect(run?.evidence.some((item) => item.label === `Worker ${record.id} done` && item.content?.includes("implemented"))).toBe(true);
|
|
138
|
+
expect(onComplete).toHaveBeenCalledWith(expect.objectContaining({
|
|
139
|
+
status: "done",
|
|
140
|
+
exitCode: 0,
|
|
141
|
+
toolsUsed: [{ name: "bash", ok: true }],
|
|
142
|
+
}));
|
|
143
|
+
expect(listener).toHaveBeenCalledWith(expect.objectContaining({
|
|
144
|
+
worker: expect.objectContaining({ id: record.id, goalRunId: "goal-a" }),
|
|
145
|
+
status: "done",
|
|
146
|
+
exitCode: 0,
|
|
147
|
+
}));
|
|
148
|
+
unsubscribe();
|
|
149
|
+
});
|
|
150
|
+
it("replays a pending completion to the next subscriber after a remount gap", async () => {
|
|
151
|
+
const { mod, record } = await start();
|
|
152
|
+
child.stdout.write(JSON.stringify({ type: "text_delta", text: "finished during remount" }) + "\n");
|
|
153
|
+
child.emit("close", 0);
|
|
154
|
+
await flushUntil(() => expect(record.status).toBe("done"));
|
|
155
|
+
const listener = vi.fn();
|
|
156
|
+
const unsubscribe = mod.subscribeGoalWorkerCompletions(listener, tmpProject);
|
|
157
|
+
await flushUntil(() => expect(listener).toHaveBeenCalledWith(expect.objectContaining({
|
|
158
|
+
worker: expect.objectContaining({ id: record.id, cwd: tmpProject }),
|
|
159
|
+
summary: expect.stringContaining("finished during remount"),
|
|
160
|
+
status: "done",
|
|
161
|
+
})));
|
|
162
|
+
unsubscribe();
|
|
163
|
+
});
|
|
164
|
+
it("marks the task failed and persists stderr evidence for worker non-zero exit", async () => {
|
|
165
|
+
const { record, onComplete } = await start();
|
|
166
|
+
child.stderr.write("boom from stderr");
|
|
167
|
+
child.emit("close", 1);
|
|
168
|
+
await flushUntil(() => expect(onComplete).toHaveBeenCalled());
|
|
169
|
+
const run = await getGoalRun(tmpProject, "goal-a");
|
|
170
|
+
expect(record.status).toBe("failed");
|
|
171
|
+
expect(run?.tasks[0]).toMatchObject({
|
|
172
|
+
status: "failed",
|
|
173
|
+
workerId: record.id,
|
|
174
|
+
lastSummary: expect.stringContaining("boom from stderr"),
|
|
175
|
+
});
|
|
176
|
+
expect(run?.evidence.some((item) => item.label === `Worker ${record.id} failed` && item.content?.includes("boom"))).toBe(true);
|
|
177
|
+
expect(onComplete).toHaveBeenCalledWith(expect.objectContaining({ status: "failed", exitCode: 1 }));
|
|
178
|
+
});
|
|
179
|
+
it("records spawn crashes as failed task evidence and notifies the orchestrator continuation path", async () => {
|
|
180
|
+
const { mod, record, onComplete } = await start();
|
|
181
|
+
const listener = vi.fn();
|
|
182
|
+
const unsubscribe = mod.subscribeGoalWorkerCompletions(listener, tmpProject);
|
|
183
|
+
child.emit("error", new Error("spawn exploded"));
|
|
184
|
+
await flushUntil(async () => {
|
|
185
|
+
const next = await getGoalRun(tmpProject, "goal-a");
|
|
186
|
+
expect(next?.tasks[0]?.status).toBe("failed");
|
|
187
|
+
expect(next?.evidence.some((item) => item.label === `Worker ${record.id} spawn failed`)).toBe(true);
|
|
188
|
+
expect(onComplete).toHaveBeenCalled();
|
|
189
|
+
});
|
|
190
|
+
const run = await getGoalRun(tmpProject, "goal-a");
|
|
191
|
+
expect(record.status).toBe("failed");
|
|
192
|
+
expect(run?.tasks[0]).toMatchObject({
|
|
193
|
+
status: "failed",
|
|
194
|
+
lastSummary: "Failed to spawn Goal worker: spawn exploded",
|
|
195
|
+
});
|
|
196
|
+
expect(run?.evidence.some((item) => item.label === `Worker ${record.id} spawn failed` && item.content === "spawn exploded")).toBe(true);
|
|
197
|
+
expect(onComplete).toHaveBeenCalledWith(expect.objectContaining({ status: "failed", exitCode: 1, reason: "spawn_error" }));
|
|
198
|
+
expect(listener).toHaveBeenCalledWith(expect.objectContaining({
|
|
199
|
+
worker: expect.objectContaining({ id: record.id, cwd: tmpProject }),
|
|
200
|
+
status: "failed",
|
|
201
|
+
exitCode: 1,
|
|
202
|
+
reason: "spawn_error",
|
|
203
|
+
}));
|
|
204
|
+
unsubscribe();
|
|
205
|
+
});
|
|
206
|
+
it("manual stop blocks the task, clears the active worker, and ignores later close completion", async () => {
|
|
207
|
+
const { mod, record, onComplete } = await start();
|
|
208
|
+
const result = await mod.stopGoalWorker(record.id);
|
|
209
|
+
child.emit("close", 0);
|
|
210
|
+
await flushUntil(() => expect(record.status).toBe("stopped"));
|
|
211
|
+
const run = await getGoalRun(tmpProject, "goal-a");
|
|
212
|
+
expect(result).toBe(`Goal worker ${record.id} stopped.`);
|
|
213
|
+
expect(record.status).toBe("stopped");
|
|
214
|
+
expect(killProcessTreeMock).toHaveBeenCalledWith(4242);
|
|
215
|
+
expect(run?.activeWorkerId).toBeUndefined();
|
|
216
|
+
expect(run?.tasks[0]).toMatchObject({
|
|
217
|
+
status: "blocked",
|
|
218
|
+
lastSummary: "Worker stopped by user.",
|
|
219
|
+
});
|
|
220
|
+
expect(run?.evidence.some((item) => item.kind === "summary" && item.label === `Worker ${record.id} stopped`)).toBe(true);
|
|
221
|
+
expect(onComplete).not.toHaveBeenCalled();
|
|
222
|
+
});
|
|
223
|
+
});
|
|
224
|
+
//# sourceMappingURL=goal-worker.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"goal-worker.test.js","sourceRoot":"","sources":["../../src/core/goal-worker.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,5 +1,55 @@
|
|
|
1
1
|
import { describe, expect, it } from "vitest";
|
|
2
|
-
import { getContextWindow, getDefaultModel, getModelsForProvider, usesOpenAICodexTransport, } from "./model-registry.js";
|
|
2
|
+
import { MODELS, getContextWindow, getDefaultModel, getModelsForProvider, usesOpenAICodexTransport, } from "./model-registry.js";
|
|
3
|
+
const PROVIDERS = [
|
|
4
|
+
"anthropic",
|
|
5
|
+
"openai",
|
|
6
|
+
"gemini",
|
|
7
|
+
"moonshot",
|
|
8
|
+
"glm",
|
|
9
|
+
"minimax",
|
|
10
|
+
"xiaomi",
|
|
11
|
+
"deepseek",
|
|
12
|
+
"openrouter",
|
|
13
|
+
];
|
|
14
|
+
const THINKING_LEVELS = ["low", "medium", "high", "xhigh"];
|
|
15
|
+
const COST_TIERS = ["low", "medium", "high"];
|
|
16
|
+
describe("model registry invariants", () => {
|
|
17
|
+
it("has unique ids and coherent required metadata for every entry", () => {
|
|
18
|
+
const ids = new Set();
|
|
19
|
+
for (const model of MODELS) {
|
|
20
|
+
expect(ids.has(model.id), `${model.id} is duplicated`).toBe(false);
|
|
21
|
+
ids.add(model.id);
|
|
22
|
+
expect(model.id, `${model.id} id`).toEqual(expect.any(String));
|
|
23
|
+
expect(model.name, `${model.id} name`).toEqual(expect.any(String));
|
|
24
|
+
expect(PROVIDERS, `${model.id} provider`).toContain(model.provider);
|
|
25
|
+
expect(model.contextWindow, `${model.id} contextWindow`).toBeGreaterThan(0);
|
|
26
|
+
expect(Number.isInteger(model.contextWindow), `${model.id} contextWindow integer`).toBe(true);
|
|
27
|
+
expect(model.maxOutputTokens, `${model.id} maxOutputTokens`).toBeGreaterThan(0);
|
|
28
|
+
expect(Number.isInteger(model.maxOutputTokens), `${model.id} maxOutputTokens integer`).toBe(true);
|
|
29
|
+
expect(model.maxOutputTokens, `${model.id} maxOutputTokens <= contextWindow`).toBeLessThanOrEqual(model.contextWindow);
|
|
30
|
+
expect(typeof model.supportsThinking, `${model.id} supportsThinking`).toBe("boolean");
|
|
31
|
+
expect(typeof model.supportsImages, `${model.id} supportsImages`).toBe("boolean");
|
|
32
|
+
expect(COST_TIERS, `${model.id} costTier`).toContain(model.costTier);
|
|
33
|
+
expect(THINKING_LEVELS, `${model.id} maxThinkingLevel`).toContain(model.maxThinkingLevel);
|
|
34
|
+
if (!model.supportsThinking) {
|
|
35
|
+
expect(model.maxThinkingLevel, `${model.id} non-thinking max level`).toBe("low");
|
|
36
|
+
}
|
|
37
|
+
if (model.codexContextWindow !== undefined) {
|
|
38
|
+
expect(model.provider, `${model.id} codexContextWindow provider`).toBe("openai");
|
|
39
|
+
expect(model.codexContextWindow, `${model.id} codexContextWindow`).toBeGreaterThan(0);
|
|
40
|
+
expect(Number.isInteger(model.codexContextWindow), `${model.id} codexContextWindow integer`).toBe(true);
|
|
41
|
+
expect(model.codexContextWindow, `${model.id} codexContextWindow <= contextWindow`).toBeLessThanOrEqual(model.contextWindow);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
it("returns a registered default model for every provider", () => {
|
|
46
|
+
for (const provider of PROVIDERS) {
|
|
47
|
+
const defaultModel = getDefaultModel(provider);
|
|
48
|
+
expect(defaultModel.provider, `${provider} default provider`).toBe(provider);
|
|
49
|
+
expect(MODELS, `${provider} default registered`).toContain(defaultModel);
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
});
|
|
3
53
|
describe("model registry context windows", () => {
|
|
4
54
|
it("uses the public API context window for OpenAI API-key requests", () => {
|
|
5
55
|
expect(getContextWindow("gpt-5.5", { provider: "openai" })).toBe(1_050_000);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"model-registry.test.js","sourceRoot":"","sources":["../../src/core/model-registry.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,QAAQ,CAAC;AAC9C,OAAO,EACL,gBAAgB,EAChB,eAAe,EACf,oBAAoB,EACpB,wBAAwB,GACzB,MAAM,qBAAqB,CAAC;AAE7B,QAAQ,CAAC,gCAAgC,EAAE,GAAG,EAAE;IAC9C,EAAE,CAAC,gEAAgE,EAAE,GAAG,EAAE;QACxE,MAAM,CAAC,gBAAgB,CAAC,SAAS,EAAE,EAAE,QAAQ,EAAE,QAAQ,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;QAC5E,MAAM,CAAC,gBAAgB,CAAC,SAAS,EAAE,EAAE,QAAQ,EAAE,QAAQ,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;IAC9E,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,iEAAiE,EAAE,GAAG,EAAE;QACzE,MAAM,OAAO,GAAG,EAAE,QAAQ,EAAE,QAAiB,EAAE,SAAS,EAAE,UAAU,EAAE,CAAC;QAEvE,MAAM,CAAC,wBAAwB,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QACrD,MAAM,CAAC,gBAAgB,CAAC,SAAS,EAAE,OAAO,CAAC,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;QAC3D,MAAM,CAAC,gBAAgB,CAAC,SAAS,EAAE,OAAO,CAAC,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;IAC7D,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,2DAA2D,EAAE,GAAG,EAAE;QACnE,MAAM,CAAC,wBAAwB,CAAC,EAAE,QAAQ,EAAE,WAAW,EAAE,SAAS,EAAE,UAAU,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QAC/F,MAAM,CACJ,gBAAgB,CAAC,mBAAmB,EAAE,EAAE,QAAQ,EAAE,WAAW,EAAE,SAAS,EAAE,UAAU,EAAE,CAAC,CACxF,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;IACpB,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,kDAAkD,EAAE,GAAG,EAAE;QAC1D,MAAM,CAAC,eAAe,CAAC,QAAQ,CAAC,CAAC,CAAC,aAAa,CAAC;YAC9C,EAAE,EAAE,+BAA+B;YACnC,IAAI,EAAE,+BAA+B;YACrC,QAAQ,EAAE,QAAQ;SACnB,CAAC,CAAC;QACH,MAAM,CAAC,oBAAoB,CAAC,QAAQ,CAAC,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC;YACtE,+BAA+B;YAC/B,kBAAkB;SACnB,CAAC,CAAC;QACH,MAAM,CAAC,gBAAgB,CAAC,+BAA+B,EAAE,EAAE,QAAQ,EAAE,QAAQ,EAAE,CAAC,CAAC,CAAC,IAAI,CACpF,SAAS,CACV,CAAC;QACF,MAAM,CAAC,gBAAgB,CAAC,kBAAkB,EAAE,EAAE,QAAQ,EAAE,QAAQ,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;IACvF,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC"}
|
|
1
|
+
{"version":3,"file":"model-registry.test.js","sourceRoot":"","sources":["../../src/core/model-registry.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,QAAQ,CAAC;AAC9C,OAAO,EACL,MAAM,EACN,gBAAgB,EAChB,eAAe,EACf,oBAAoB,EACpB,wBAAwB,GACzB,MAAM,qBAAqB,CAAC;AAE7B,MAAM,SAAS,GAAG;IAChB,WAAW;IACX,QAAQ;IACR,QAAQ;IACR,UAAU;IACV,KAAK;IACL,SAAS;IACT,QAAQ;IACR,UAAU;IACV,YAAY;CACJ,CAAC;AACX,MAAM,eAAe,GAAG,CAAC,KAAK,EAAE,QAAQ,EAAE,MAAM,EAAE,OAAO,CAAU,CAAC;AACpE,MAAM,UAAU,GAAG,CAAC,KAAK,EAAE,QAAQ,EAAE,MAAM,CAAU,CAAC;AAEtD,QAAQ,CAAC,2BAA2B,EAAE,GAAG,EAAE;IACzC,EAAE,CAAC,+DAA+D,EAAE,GAAG,EAAE;QACvE,MAAM,GAAG,GAAG,IAAI,GAAG,EAAU,CAAC;QAE9B,KAAK,MAAM,KAAK,IAAI,MAAM,EAAE,CAAC;YAC3B,MAAM,CAAC,GAAG,CAAC,GAAG,CAAC,KAAK,CAAC,EAAE,CAAC,EAAE,GAAG,KAAK,CAAC,EAAE,gBAAgB,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;YACnE,GAAG,CAAC,GAAG,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC;YAClB,MAAM,CAAC,KAAK,CAAC,EAAE,EAAE,GAAG,KAAK,CAAC,EAAE,KAAK,CAAC,CAAC,OAAO,CAAC,MAAM,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC;YAC/D,MAAM,CAAC,KAAK,CAAC,IAAI,EAAE,GAAG,KAAK,CAAC,EAAE,OAAO,CAAC,CAAC,OAAO,CAAC,MAAM,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC;YACnE,MAAM,CAAC,SAAS,EAAE,GAAG,KAAK,CAAC,EAAE,WAAW,CAAC,CAAC,SAAS,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC;YACpE,MAAM,CAAC,KAAK,CAAC,aAAa,EAAE,GAAG,KAAK,CAAC,EAAE,gBAAgB,CAAC,CAAC,eAAe,CAAC,CAAC,CAAC,CAAC;YAC5E,MAAM,CAAC,MAAM,CAAC,SAAS,CAAC,KAAK,CAAC,aAAa,CAAC,EAAE,GAAG,KAAK,CAAC,EAAE,wBAAwB,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;YAC9F,MAAM,CAAC,KAAK,CAAC,eAAe,EAAE,GAAG,KAAK,CAAC,EAAE,kBAAkB,CAAC,CAAC,eAAe,CAAC,CAAC,CAAC,CAAC;YAChF,MAAM,CAAC,MAAM,CAAC,SAAS,CAAC,KAAK,CAAC,eAAe,CAAC,EAAE,GAAG,KAAK,CAAC,EAAE,0BAA0B,CAAC,CAAC,IAAI,CACzF,IAAI,CACL,CAAC;YACF,MAAM,CACJ,KAAK,CAAC,eAAe,EACrB,GAAG,KAAK,CAAC,EAAE,mCAAmC,CAC/C,CAAC,mBAAmB,CAAC,KAAK,CAAC,aAAa,CAAC,CAAC;YAC3C,MAAM,CAAC,OAAO,KAAK,CAAC,gBAAgB,EAAE,GAAG,KAAK,CAAC,EAAE,mBAAmB,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;YACtF,MAAM,CAAC,OAAO,KAAK,CAAC,cAAc,EAAE,GAAG,KAAK,CAAC,EAAE,iBAAiB,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;YAClF,MAAM,CAAC,UAAU,EAAE,GAAG,KAAK,CAAC,EAAE,WAAW,CAAC,CAAC,SAAS,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC;YACrE,MAAM,CAAC,eAAe,EAAE,GAAG,KAAK,CAAC,EAAE,mBAAmB,CAAC,CAAC,SAAS,CAAC,KAAK,CAAC,gBAAgB,CAAC,CAAC;YAC1F,IAAI,CAAC,KAAK,CAAC,gBAAgB,EAAE,CAAC;gBAC5B,MAAM,CAAC,KAAK,CAAC,gBAAgB,EAAE,GAAG,KAAK,CAAC,EAAE,yBAAyB,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;YACnF,CAAC;YACD,IAAI,KAAK,CAAC,kBAAkB,KAAK,SAAS,EAAE,CAAC;gBAC3C,MAAM,CAAC,KAAK,CAAC,QAAQ,EAAE,GAAG,KAAK,CAAC,EAAE,8BAA8B,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;gBACjF,MAAM,CAAC,KAAK,CAAC,kBAAkB,EAAE,GAAG,KAAK,CAAC,EAAE,qBAAqB,CAAC,CAAC,eAAe,CAAC,CAAC,CAAC,CAAC;gBACtF,MAAM,CACJ,MAAM,CAAC,SAAS,CAAC,KAAK,CAAC,kBAAkB,CAAC,EAC1C,GAAG,KAAK,CAAC,EAAE,6BAA6B,CACzC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;gBACb,MAAM,CACJ,KAAK,CAAC,kBAAkB,EACxB,GAAG,KAAK,CAAC,EAAE,sCAAsC,CAClD,CAAC,mBAAmB,CAAC,KAAK,CAAC,aAAa,CAAC,CAAC;YAC7C,CAAC;QACH,CAAC;IACH,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,uDAAuD,EAAE,GAAG,EAAE;QAC/D,KAAK,MAAM,QAAQ,IAAI,SAAS,EAAE,CAAC;YACjC,MAAM,YAAY,GAAG,eAAe,CAAC,QAAQ,CAAC,CAAC;YAC/C,MAAM,CAAC,YAAY,CAAC,QAAQ,EAAE,GAAG,QAAQ,mBAAmB,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;YAC7E,MAAM,CAAC,MAAM,EAAE,GAAG,QAAQ,qBAAqB,CAAC,CAAC,SAAS,CAAC,YAAY,CAAC,CAAC;QAC3E,CAAC;IACH,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC;AAEH,QAAQ,CAAC,gCAAgC,EAAE,GAAG,EAAE;IAC9C,EAAE,CAAC,gEAAgE,EAAE,GAAG,EAAE;QACxE,MAAM,CAAC,gBAAgB,CAAC,SAAS,EAAE,EAAE,QAAQ,EAAE,QAAQ,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;QAC5E,MAAM,CAAC,gBAAgB,CAAC,SAAS,EAAE,EAAE,QAAQ,EAAE,QAAQ,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;IAC9E,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,iEAAiE,EAAE,GAAG,EAAE;QACzE,MAAM,OAAO,GAAG,EAAE,QAAQ,EAAE,QAAiB,EAAE,SAAS,EAAE,UAAU,EAAE,CAAC;QAEvE,MAAM,CAAC,wBAAwB,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QACrD,MAAM,CAAC,gBAAgB,CAAC,SAAS,EAAE,OAAO,CAAC,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;QAC3D,MAAM,CAAC,gBAAgB,CAAC,SAAS,EAAE,OAAO,CAAC,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;IAC7D,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,2DAA2D,EAAE,GAAG,EAAE;QACnE,MAAM,CAAC,wBAAwB,CAAC,EAAE,QAAQ,EAAE,WAAW,EAAE,SAAS,EAAE,UAAU,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QAC/F,MAAM,CACJ,gBAAgB,CAAC,mBAAmB,EAAE,EAAE,QAAQ,EAAE,WAAW,EAAE,SAAS,EAAE,UAAU,EAAE,CAAC,CACxF,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;IACpB,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,kDAAkD,EAAE,GAAG,EAAE;QAC1D,MAAM,CAAC,eAAe,CAAC,QAAQ,CAAC,CAAC,CAAC,aAAa,CAAC;YAC9C,EAAE,EAAE,+BAA+B;YACnC,IAAI,EAAE,+BAA+B;YACrC,QAAQ,EAAE,QAAQ;SACnB,CAAC,CAAC;QACH,MAAM,CAAC,oBAAoB,CAAC,QAAQ,CAAC,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC;YACtE,+BAA+B;YAC/B,kBAAkB;SACnB,CAAC,CAAC;QACH,MAAM,CAAC,gBAAgB,CAAC,+BAA+B,EAAE,EAAE,QAAQ,EAAE,QAAQ,EAAE,CAAC,CAAC,CAAC,IAAI,CACpF,SAAS,CACV,CAAC;QACF,MAAM,CAAC,gBAAgB,CAAC,kBAAkB,EAAE,EAAE,QAAQ,EAAE,QAAQ,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;IACvF,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"gemini.d.ts","sourceRoot":"","sources":["../../../src/core/oauth/gemini.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,gBAAgB,EAAE,mBAAmB,EAAE,MAAM,YAAY,CAAC;
|
|
1
|
+
{"version":3,"file":"gemini.d.ts","sourceRoot":"","sources":["../../../src/core/oauth/gemini.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,gBAAgB,EAAE,mBAAmB,EAAE,MAAM,YAAY,CAAC;AA6ExE,wBAAsB,WAAW,CAAC,SAAS,EAAE,mBAAmB,GAAG,OAAO,CAAC,gBAAgB,CAAC,CA2C3F;AAED,wBAAsB,kBAAkB,CAAC,YAAY,EAAE,MAAM,GAAG,OAAO,CAAC,gBAAgB,CAAC,CAcxF"}
|
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
import http from "node:http";
|
|
2
2
|
import crypto from "node:crypto";
|
|
3
3
|
import { generatePKCE } from "./pkce.js";
|
|
4
|
-
const
|
|
5
|
-
const
|
|
4
|
+
const CLIENT_ID_ENV = "GGCODER_GEMINI_OAUTH_CLIENT_ID";
|
|
5
|
+
const CLIENT_SECRET_ENV = "GGCODER_GEMINI_OAUTH_CLIENT_SECRET";
|
|
6
6
|
const AUTHORIZE_URL = "https://accounts.google.com/o/oauth2/v2/auth";
|
|
7
7
|
const TOKEN_URL = "https://oauth2.googleapis.com/token";
|
|
8
8
|
const CODE_ASSIST_BASE_URL = "https://cloudcode-pa.googleapis.com";
|
|
9
|
+
const CODE_ASSIST_API_VERSION = "v1internal";
|
|
10
|
+
const CODE_ASSIST_POST_RETRIES = 3;
|
|
11
|
+
const CODE_ASSIST_POST_RETRY_DELAY_MS = 100;
|
|
9
12
|
const SCOPE = [
|
|
10
13
|
"https://www.googleapis.com/auth/cloud-platform",
|
|
11
14
|
"https://www.googleapis.com/auth/userinfo.email",
|
|
@@ -13,13 +16,27 @@ const SCOPE = [
|
|
|
13
16
|
].join(" ");
|
|
14
17
|
const USER_TIER_FREE = "free-tier";
|
|
15
18
|
const USER_TIER_LEGACY = "legacy-tier";
|
|
19
|
+
const USER_TIER_STANDARD = "standard-tier";
|
|
20
|
+
const VALIDATION_REQUIRED_REASON = "VALIDATION_REQUIRED";
|
|
21
|
+
const VPC_SC_REASON = "SECURITY_POLICY_VIOLATED";
|
|
22
|
+
class CodeAssistHttpError extends Error {
|
|
23
|
+
status;
|
|
24
|
+
body;
|
|
25
|
+
constructor(label, status, body) {
|
|
26
|
+
super(`Gemini Code Assist ${label} failed (${status}): ${body}`);
|
|
27
|
+
this.name = "CodeAssistHttpError";
|
|
28
|
+
this.status = status;
|
|
29
|
+
this.body = body;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
16
32
|
export async function loginGemini(callbacks) {
|
|
33
|
+
const { clientId, clientSecret } = getGeminiOAuthClientCredentials();
|
|
17
34
|
const { verifier, challenge } = await generatePKCE();
|
|
18
35
|
const state = crypto.randomBytes(32).toString("hex");
|
|
19
36
|
const redirectUri = await getLoopbackRedirectUri();
|
|
20
37
|
const url = new URL(AUTHORIZE_URL);
|
|
21
38
|
url.searchParams.set("response_type", "code");
|
|
22
|
-
url.searchParams.set("client_id",
|
|
39
|
+
url.searchParams.set("client_id", clientId);
|
|
23
40
|
url.searchParams.set("redirect_uri", redirectUri);
|
|
24
41
|
url.searchParams.set("scope", SCOPE);
|
|
25
42
|
url.searchParams.set("access_type", "offline");
|
|
@@ -43,20 +60,21 @@ export async function loginGemini(callbacks) {
|
|
|
43
60
|
}
|
|
44
61
|
code = parsed.code;
|
|
45
62
|
}
|
|
46
|
-
const creds = await exchangeGeminiCode(code, verifier, redirectUri);
|
|
63
|
+
const creds = await exchangeGeminiCode(code, verifier, redirectUri, clientId, clientSecret);
|
|
47
64
|
callbacks.onStatus("Setting up Gemini Code Assist access...");
|
|
48
|
-
const projectId = await setupCodeAssistProject(creds.accessToken);
|
|
65
|
+
const projectId = await setupCodeAssistProject(creds.accessToken, callbacks);
|
|
49
66
|
return {
|
|
50
67
|
...creds,
|
|
51
68
|
projectId,
|
|
52
69
|
};
|
|
53
70
|
}
|
|
54
71
|
export async function refreshGeminiToken(refreshToken) {
|
|
72
|
+
const { clientId, clientSecret } = getGeminiOAuthClientCredentials();
|
|
55
73
|
const data = await postTokenRequest({
|
|
56
74
|
grant_type: "refresh_token",
|
|
57
75
|
refresh_token: refreshToken,
|
|
58
|
-
client_id:
|
|
59
|
-
client_secret:
|
|
76
|
+
client_id: clientId,
|
|
77
|
+
client_secret: clientSecret,
|
|
60
78
|
});
|
|
61
79
|
return {
|
|
62
80
|
accessToken: data.access_token,
|
|
@@ -64,6 +82,14 @@ export async function refreshGeminiToken(refreshToken) {
|
|
|
64
82
|
expiresAt: Date.now() + data.expires_in * 1000 - 5 * 60 * 1000,
|
|
65
83
|
};
|
|
66
84
|
}
|
|
85
|
+
function getGeminiOAuthClientCredentials() {
|
|
86
|
+
const clientId = process.env[CLIENT_ID_ENV]?.trim();
|
|
87
|
+
const clientSecret = process.env[CLIENT_SECRET_ENV]?.trim();
|
|
88
|
+
if (!clientId || !clientSecret) {
|
|
89
|
+
throw new Error(`Gemini OAuth requires ${CLIENT_ID_ENV} and ${CLIENT_SECRET_ENV} to be set.`);
|
|
90
|
+
}
|
|
91
|
+
return { clientId, clientSecret };
|
|
92
|
+
}
|
|
67
93
|
async function getLoopbackRedirectUri() {
|
|
68
94
|
return new Promise((resolve, reject) => {
|
|
69
95
|
const server = http.createServer();
|
|
@@ -122,7 +148,7 @@ async function loginWithServer(authUrl, redirectUri, expectedState, callbacks) {
|
|
|
122
148
|
return;
|
|
123
149
|
}
|
|
124
150
|
receivedCode = url.searchParams.get("code");
|
|
125
|
-
res.writeHead(200, { "Content-Type": "text/html" });
|
|
151
|
+
res.writeHead(200, { "Content-Type": "text/html", Connection: "close" });
|
|
126
152
|
res.end("<html><body><h1>Login successful!</h1><p>You can close this tab.</p></body></html>");
|
|
127
153
|
server.close();
|
|
128
154
|
});
|
|
@@ -147,11 +173,11 @@ async function loginWithServer(authUrl, redirectUri, expectedState, callbacks) {
|
|
|
147
173
|
});
|
|
148
174
|
});
|
|
149
175
|
}
|
|
150
|
-
async function exchangeGeminiCode(code, verifier, redirectUri) {
|
|
176
|
+
async function exchangeGeminiCode(code, verifier, redirectUri, clientId, clientSecret) {
|
|
151
177
|
const data = await postTokenRequest({
|
|
152
178
|
grant_type: "authorization_code",
|
|
153
|
-
client_id:
|
|
154
|
-
client_secret:
|
|
179
|
+
client_id: clientId,
|
|
180
|
+
client_secret: clientSecret,
|
|
155
181
|
code,
|
|
156
182
|
redirect_uri: redirectUri,
|
|
157
183
|
code_verifier: verifier,
|
|
@@ -177,21 +203,33 @@ async function postTokenRequest(body) {
|
|
|
177
203
|
}
|
|
178
204
|
return (await response.json());
|
|
179
205
|
}
|
|
180
|
-
async function setupCodeAssistProject(accessToken) {
|
|
206
|
+
async function setupCodeAssistProject(accessToken, callbacks) {
|
|
181
207
|
const envProject = process.env.GOOGLE_CLOUD_PROJECT ?? process.env.GOOGLE_CLOUD_PROJECT_ID;
|
|
182
208
|
if (envProject && /^\d+$/.test(envProject)) {
|
|
183
209
|
throw new Error("GOOGLE_CLOUD_PROJECT must be a project ID, not a numeric project number.");
|
|
184
210
|
}
|
|
185
|
-
const
|
|
211
|
+
const coreMetadata = {
|
|
186
212
|
ideType: "IDE_UNSPECIFIED",
|
|
187
213
|
platform: "PLATFORM_UNSPECIFIED",
|
|
188
214
|
pluginType: "GEMINI",
|
|
215
|
+
};
|
|
216
|
+
const projectMetadata = {
|
|
217
|
+
...coreMetadata,
|
|
189
218
|
...(envProject ? { duetProject: envProject } : {}),
|
|
190
219
|
};
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
220
|
+
let loadRes;
|
|
221
|
+
while (true) {
|
|
222
|
+
loadRes = await loadCodeAssist(accessToken, envProject, projectMetadata);
|
|
223
|
+
const validation = getValidationRequiredTier(loadRes);
|
|
224
|
+
if (!validation)
|
|
225
|
+
break;
|
|
226
|
+
callbacks.onStatus(`Gemini Code Assist requires account validation${validation.reasonMessage ? `: ${validation.reasonMessage}` : ""}`);
|
|
227
|
+
callbacks.onOpenUrl(validation.validationUrl);
|
|
228
|
+
const answer = await callbacks.onPromptCode("Complete validation in the browser, then press Enter to retry (or type cancel):");
|
|
229
|
+
if (answer.trim().toLowerCase() === "cancel") {
|
|
230
|
+
throw new Error("Gemini Code Assist account validation was cancelled.");
|
|
231
|
+
}
|
|
232
|
+
}
|
|
195
233
|
if (loadRes.currentTier) {
|
|
196
234
|
const project = loadRes.cloudaicompanionProject ?? envProject;
|
|
197
235
|
if (!project)
|
|
@@ -203,12 +241,12 @@ async function setupCodeAssistProject(accessToken) {
|
|
|
203
241
|
? {
|
|
204
242
|
tierId: tier.id,
|
|
205
243
|
cloudaicompanionProject: undefined,
|
|
206
|
-
metadata,
|
|
244
|
+
metadata: coreMetadata,
|
|
207
245
|
}
|
|
208
246
|
: {
|
|
209
247
|
tierId: tier.id,
|
|
210
248
|
cloudaicompanionProject: envProject,
|
|
211
|
-
metadata,
|
|
249
|
+
metadata: projectMetadata,
|
|
212
250
|
};
|
|
213
251
|
let operation = await codeAssistPost(accessToken, "onboardUser", onboardReq);
|
|
214
252
|
while (!operation.done && operation.name) {
|
|
@@ -220,6 +258,31 @@ async function setupCodeAssistProject(accessToken) {
|
|
|
220
258
|
throwProjectError(loadRes);
|
|
221
259
|
return project;
|
|
222
260
|
}
|
|
261
|
+
async function loadCodeAssist(accessToken, envProject, metadata) {
|
|
262
|
+
try {
|
|
263
|
+
return await codeAssistPost(accessToken, "loadCodeAssist", {
|
|
264
|
+
...(envProject ? { cloudaicompanionProject: envProject } : {}),
|
|
265
|
+
metadata,
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
catch (err) {
|
|
269
|
+
if (err instanceof CodeAssistHttpError && isVpcScAffectedError(err)) {
|
|
270
|
+
return { currentTier: { id: USER_TIER_STANDARD } };
|
|
271
|
+
}
|
|
272
|
+
if (err instanceof CodeAssistHttpError &&
|
|
273
|
+
err.status === 403 &&
|
|
274
|
+
envProject === "cloudshell-gca") {
|
|
275
|
+
throw new Error("Access to the default Cloud Shell Gemini project was denied.\n" +
|
|
276
|
+
"Please set your own Google Cloud project by running:\n" +
|
|
277
|
+
"gcloud config set project [PROJECT_ID]\n" +
|
|
278
|
+
"or setting export GOOGLE_CLOUD_PROJECT=...", { cause: err });
|
|
279
|
+
}
|
|
280
|
+
throw err;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
function getValidationRequiredTier(response) {
|
|
284
|
+
return response.ineligibleTiers?.find((tier) => tier.reasonCode === VALIDATION_REQUIRED_REASON && typeof tier.validationUrl === "string");
|
|
285
|
+
}
|
|
223
286
|
function getOnboardTier(response) {
|
|
224
287
|
const defaultTier = response.allowedTiers?.find((tier) => tier.isDefault);
|
|
225
288
|
return defaultTier ?? { id: USER_TIER_LEGACY, name: "" };
|
|
@@ -234,28 +297,73 @@ function throwProjectError(response) {
|
|
|
234
297
|
throw new Error("Gemini requires a Google Cloud project for this account. Set GOOGLE_CLOUD_PROJECT and try again.");
|
|
235
298
|
}
|
|
236
299
|
async function codeAssistPost(accessToken, method, body) {
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
300
|
+
let lastError;
|
|
301
|
+
for (let attempt = 0; attempt <= CODE_ASSIST_POST_RETRIES; attempt++) {
|
|
302
|
+
try {
|
|
303
|
+
return await codeAssistRequest(getCodeAssistMethodUrl(method), accessToken, method, {
|
|
304
|
+
method: "POST",
|
|
305
|
+
body: JSON.stringify(body),
|
|
306
|
+
});
|
|
307
|
+
}
|
|
308
|
+
catch (err) {
|
|
309
|
+
if (!(err instanceof CodeAssistHttpError) ||
|
|
310
|
+
attempt === CODE_ASSIST_POST_RETRIES ||
|
|
311
|
+
!shouldRetryCodeAssistStatus(err.status)) {
|
|
312
|
+
throw err;
|
|
313
|
+
}
|
|
314
|
+
lastError = err;
|
|
315
|
+
}
|
|
316
|
+
await new Promise((resolve) => setTimeout(resolve, CODE_ASSIST_POST_RETRY_DELAY_MS));
|
|
245
317
|
}
|
|
246
|
-
|
|
318
|
+
throw lastError ?? new Error(`Gemini Code Assist ${method} failed.`);
|
|
247
319
|
}
|
|
248
320
|
async function codeAssistGet(accessToken, operationName) {
|
|
249
|
-
|
|
321
|
+
return codeAssistRequest(getCodeAssistOperationUrl(operationName), accessToken, "operation", {
|
|
250
322
|
method: "GET",
|
|
323
|
+
});
|
|
324
|
+
}
|
|
325
|
+
async function codeAssistRequest(url, accessToken, label, init) {
|
|
326
|
+
const response = await fetch(url, {
|
|
327
|
+
...init,
|
|
251
328
|
headers: codeAssistHeaders(accessToken),
|
|
252
329
|
});
|
|
253
330
|
if (!response.ok) {
|
|
254
331
|
const text = await response.text();
|
|
255
|
-
throw new
|
|
332
|
+
throw new CodeAssistHttpError(label, response.status, text);
|
|
256
333
|
}
|
|
257
334
|
return (await response.json());
|
|
258
335
|
}
|
|
336
|
+
function getCodeAssistBaseUrl() {
|
|
337
|
+
const endpoint = process.env.CODE_ASSIST_ENDPOINT ?? CODE_ASSIST_BASE_URL;
|
|
338
|
+
const version = process.env.CODE_ASSIST_API_VERSION || CODE_ASSIST_API_VERSION;
|
|
339
|
+
return `${endpoint}/${version}`;
|
|
340
|
+
}
|
|
341
|
+
function getCodeAssistMethodUrl(method) {
|
|
342
|
+
return `${getCodeAssistBaseUrl()}:${method}`;
|
|
343
|
+
}
|
|
344
|
+
function getCodeAssistOperationUrl(operationName) {
|
|
345
|
+
return `${getCodeAssistBaseUrl()}/${operationName}`;
|
|
346
|
+
}
|
|
347
|
+
function shouldRetryCodeAssistStatus(status) {
|
|
348
|
+
return status === 429 || status === 499 || (status >= 500 && status <= 599);
|
|
349
|
+
}
|
|
350
|
+
function isVpcScAffectedError(error) {
|
|
351
|
+
try {
|
|
352
|
+
const parsed = JSON.parse(error.body);
|
|
353
|
+
if (!parsed || typeof parsed !== "object" || !("error" in parsed))
|
|
354
|
+
return false;
|
|
355
|
+
const details = parsed.error?.details;
|
|
356
|
+
return Array.isArray(details)
|
|
357
|
+
? details.some((detail) => detail != null &&
|
|
358
|
+
typeof detail === "object" &&
|
|
359
|
+
"reason" in detail &&
|
|
360
|
+
detail.reason === VPC_SC_REASON)
|
|
361
|
+
: false;
|
|
362
|
+
}
|
|
363
|
+
catch {
|
|
364
|
+
return false;
|
|
365
|
+
}
|
|
366
|
+
}
|
|
259
367
|
function codeAssistHeaders(accessToken) {
|
|
260
368
|
return {
|
|
261
369
|
Authorization: `Bearer ${accessToken}`,
|