@stigmer/runner 3.1.5 → 3.1.7
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/.build-fingerprint +1 -1
- package/dist/activities/discover-mcp-server.js +23 -0
- package/dist/activities/discover-mcp-server.js.map +1 -1
- package/dist/activities/execute-cursor/index.js +39 -1
- package/dist/activities/execute-cursor/index.js.map +1 -1
- package/dist/activities/execute-cursor/workspace-provision.d.ts +23 -4
- package/dist/activities/execute-cursor/workspace-provision.js +17 -6
- package/dist/activities/execute-cursor/workspace-provision.js.map +1 -1
- package/dist/activities/execute-deep-agent/index.js +8 -1
- package/dist/activities/execute-deep-agent/index.js.map +1 -1
- package/dist/activities/execute-deep-agent/inline-publisher.d.ts +1 -1
- package/dist/activities/execute-deep-agent/post-stream.d.ts +1 -1
- package/dist/activities/execute-deep-agent/streaming-side-effects.d.ts +1 -1
- package/dist/activities/execute-deep-agent/streaming-terminal.d.ts +1 -1
- package/dist/activities/execute-deep-agent/streaming.d.ts +1 -1
- package/dist/activities/execute-deep-agent/v3-status-builder.d.ts +1 -1
- package/dist/shared/execution-status-writer.d.ts +31 -0
- package/dist/shared/execution-status-writer.js +43 -0
- package/dist/shared/execution-status-writer.js.map +1 -0
- package/dist/shared/mcp-oauth-detect.d.ts +53 -0
- package/dist/shared/mcp-oauth-detect.js +99 -0
- package/dist/shared/mcp-oauth-detect.js.map +1 -0
- package/dist/shared/workspace/writeback-coordinator.d.ts +120 -0
- package/dist/shared/workspace/writeback-coordinator.js +393 -0
- package/dist/shared/workspace/writeback-coordinator.js.map +1 -0
- package/package.json +2 -2
- package/src/activities/discover-mcp-server.ts +25 -0
- package/src/activities/execute-cursor/__tests__/workspace-provision.test.ts +23 -15
- package/src/activities/execute-cursor/index.ts +41 -1
- package/src/activities/execute-cursor/workspace-provision.ts +39 -7
- package/src/activities/execute-deep-agent/__tests__/post-stream.test.ts +1 -1
- package/src/activities/execute-deep-agent/index.ts +8 -1
- package/src/activities/execute-deep-agent/inline-publisher.ts +1 -1
- package/src/activities/execute-deep-agent/post-stream.ts +1 -1
- package/src/activities/execute-deep-agent/status-builder.ts +1 -1
- package/src/activities/execute-deep-agent/streaming-side-effects.ts +1 -1
- package/src/activities/execute-deep-agent/streaming-terminal.ts +1 -1
- package/src/activities/execute-deep-agent/streaming.ts +1 -1
- package/src/activities/execute-deep-agent/v3-status-builder.ts +1 -1
- package/src/shared/__tests__/mcp-oauth-detect.test.ts +147 -0
- package/src/shared/execution-status-writer.ts +56 -0
- package/src/shared/mcp-oauth-detect.ts +112 -0
- package/src/shared/workspace/__tests__/writeback-coordinator.integration.test.ts +230 -0
- package/src/shared/workspace/__tests__/writeback-coordinator.test.ts +477 -0
- package/src/{activities/execute-deep-agent → shared/workspace}/writeback-coordinator.ts +206 -94
- package/dist/activities/execute-deep-agent/execution-status-writer.d.ts +0 -17
- package/dist/activities/execute-deep-agent/execution-status-writer.js +0 -9
- package/dist/activities/execute-deep-agent/execution-status-writer.js.map +0 -1
- package/dist/activities/execute-deep-agent/writeback-coordinator.d.ts +0 -71
- package/dist/activities/execute-deep-agent/writeback-coordinator.js +0 -299
- package/dist/activities/execute-deep-agent/writeback-coordinator.js.map +0 -1
- package/src/activities/execute-deep-agent/__tests__/writeback-coordinator.test.ts +0 -426
- package/src/activities/execute-deep-agent/execution-status-writer.ts +0 -19
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Real-git integration tests for the session-scoped write-back lifecycle.
|
|
3
|
+
*
|
|
4
|
+
* A hermetic origin (local bare repo) and a working clone stand in for
|
|
5
|
+
* GitHub + the session workspace; only the PR API is mocked. These prove the
|
|
6
|
+
* behaviors the mocked unit suite cannot: that `ensureBranch`'s checkout
|
|
7
|
+
* cases and the always-`-u` push compose correctly against real git across
|
|
8
|
+
* SEQUENTIAL COORDINATOR INSTANCES (turn 1 and turn 2 of one session), and
|
|
9
|
+
* that only the tree's content at finalize — the post-reconcile, approved
|
|
10
|
+
* content — reaches the session branch.
|
|
11
|
+
*/
|
|
12
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
|
13
|
+
import { execFileSync } from "node:child_process";
|
|
14
|
+
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
15
|
+
import { join } from "node:path";
|
|
16
|
+
import { tmpdir } from "node:os";
|
|
17
|
+
import { create } from "@bufbuild/protobuf";
|
|
18
|
+
import { AgentExecutionStatusSchema } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/api_pb";
|
|
19
|
+
import { WorkspaceWriteBackPhase } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/writeback_pb";
|
|
20
|
+
import { GitWriteBackMode } from "@stigmer/protos/ai/stigmer/agentic/session/v1/enum_pb";
|
|
21
|
+
import { WriteBackCoordinator } from "../writeback-coordinator.js";
|
|
22
|
+
import { LocalWorkspaceBackend } from "../local-backend.js";
|
|
23
|
+
import { statusProtoWriter, type ExecutionStatusWriter } from "../../execution-status-writer.js";
|
|
24
|
+
import type { ProvisionResult } from "../types.js";
|
|
25
|
+
import { SourceType } from "../types.js";
|
|
26
|
+
import { AGENT_GIT_AUTHOR_EMAIL } from "../git-identity.js";
|
|
27
|
+
|
|
28
|
+
const SESSION_ID = "ses-int-01";
|
|
29
|
+
const SESSION_BRANCH = `stigmer/${SESSION_ID}`;
|
|
30
|
+
|
|
31
|
+
// Real git spawns (init/clone/commit/push, ~a dozen per test) are slow under
|
|
32
|
+
// full-suite parallel load — observed >40s wall clock at ~4s of actual work.
|
|
33
|
+
const GIT_TEST_TIMEOUT_MS = 120_000;
|
|
34
|
+
|
|
35
|
+
let tmpRoot: string;
|
|
36
|
+
let originDir: string;
|
|
37
|
+
let workDir: string;
|
|
38
|
+
|
|
39
|
+
function git(cwd: string, args: string[]): string {
|
|
40
|
+
return execFileSync("git", args, {
|
|
41
|
+
cwd,
|
|
42
|
+
encoding: "utf-8",
|
|
43
|
+
env: {
|
|
44
|
+
...process.env,
|
|
45
|
+
GIT_AUTHOR_NAME: "human",
|
|
46
|
+
GIT_AUTHOR_EMAIL: "human@example.com",
|
|
47
|
+
GIT_COMMITTER_NAME: "human",
|
|
48
|
+
GIT_COMMITTER_EMAIL: "human@example.com",
|
|
49
|
+
},
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function makeCoordinator(
|
|
54
|
+
executionId: string,
|
|
55
|
+
writer: ExecutionStatusWriter,
|
|
56
|
+
): WriteBackCoordinator {
|
|
57
|
+
const provisionResult: ProvisionResult = {
|
|
58
|
+
rootDir: workDir,
|
|
59
|
+
sourceType: SourceType.GIT_REPO,
|
|
60
|
+
consumedKeys: [],
|
|
61
|
+
workspaceDescription: "integration",
|
|
62
|
+
entryName: "repo",
|
|
63
|
+
gitMetadata: {
|
|
64
|
+
repoUrl: "https://github.com/acme/repo.git",
|
|
65
|
+
branch: "main",
|
|
66
|
+
baseCommit: git(workDir, ["rev-parse", "main"]).trim(),
|
|
67
|
+
gitCredentialsConfigured: true,
|
|
68
|
+
},
|
|
69
|
+
};
|
|
70
|
+
return new WriteBackCoordinator({
|
|
71
|
+
statusWriter: writer,
|
|
72
|
+
executionId,
|
|
73
|
+
sessionId: SESSION_ID,
|
|
74
|
+
githubToken: "ghp_test",
|
|
75
|
+
provisionResults: [provisionResult],
|
|
76
|
+
workspaceEntries: [
|
|
77
|
+
{
|
|
78
|
+
name: "repo",
|
|
79
|
+
source: {
|
|
80
|
+
source: {
|
|
81
|
+
case: "gitRepo" as const,
|
|
82
|
+
value: { writeBackMode: GitWriteBackMode.GIT_WRITE_BACK_BRANCH_AND_PR },
|
|
83
|
+
},
|
|
84
|
+
},
|
|
85
|
+
} as any,
|
|
86
|
+
],
|
|
87
|
+
workspaceBackend: new LocalWorkspaceBackend(workDir),
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** The PR API mock: no open PR on first list, then created; adopted after. */
|
|
92
|
+
function mockGithubApi(): Array<{ method: string; url: string }> {
|
|
93
|
+
const calls: Array<{ method: string; url: string }> = [];
|
|
94
|
+
let created = false;
|
|
95
|
+
globalThis.fetch = vi.fn(async (url: any, init?: any) => {
|
|
96
|
+
const method = init?.method ?? "GET";
|
|
97
|
+
calls.push({ method, url: String(url) });
|
|
98
|
+
if (method === "GET") {
|
|
99
|
+
const body = created
|
|
100
|
+
? [{ html_url: "https://github.com/acme/repo/pull/5", number: 5 }]
|
|
101
|
+
: [];
|
|
102
|
+
return new Response(JSON.stringify(body), { status: 200 });
|
|
103
|
+
}
|
|
104
|
+
created = true;
|
|
105
|
+
return new Response(
|
|
106
|
+
JSON.stringify({ html_url: "https://github.com/acme/repo/pull/5", number: 5 }),
|
|
107
|
+
{ status: 201 },
|
|
108
|
+
);
|
|
109
|
+
}) as typeof fetch;
|
|
110
|
+
return calls;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const originalFetch = globalThis.fetch;
|
|
114
|
+
|
|
115
|
+
beforeEach(() => {
|
|
116
|
+
tmpRoot = mkdtempSync(join(tmpdir(), "wb-int-"));
|
|
117
|
+
originDir = join(tmpRoot, "origin.git");
|
|
118
|
+
workDir = join(tmpRoot, "work");
|
|
119
|
+
|
|
120
|
+
execFileSync("git", ["init", "--bare", "-b", "main", originDir], { stdio: "pipe" });
|
|
121
|
+
execFileSync("git", ["clone", originDir, workDir], { stdio: "pipe" });
|
|
122
|
+
writeFileSync(join(workDir, "README.md"), "# seed\n");
|
|
123
|
+
git(workDir, ["add", "-A"]);
|
|
124
|
+
git(workDir, ["commit", "-m", "seed"]);
|
|
125
|
+
git(workDir, ["push", "-u", "origin", "main"]);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
afterEach(() => {
|
|
129
|
+
globalThis.fetch = originalFetch;
|
|
130
|
+
rmSync(tmpRoot, { recursive: true, force: true });
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
describe("WriteBackCoordinator against real git", () => {
|
|
134
|
+
it(
|
|
135
|
+
"pushes turn 1 to the session branch, then turn 2 (new instance) appends to it and adopts the PR",
|
|
136
|
+
async () => {
|
|
137
|
+
const apiCalls = mockGithubApi();
|
|
138
|
+
|
|
139
|
+
// ── Turn 1: the agent created a file; the review kept it. ──
|
|
140
|
+
writeFileSync(join(workDir, "notes.md"), "approved content\n");
|
|
141
|
+
const status1 = create(AgentExecutionStatusSchema, {});
|
|
142
|
+
const coord1 = makeCoordinator("exec-turn-1", statusProtoWriter(status1));
|
|
143
|
+
await coord1.finalize();
|
|
144
|
+
|
|
145
|
+
const wb1 = status1.workspaceWriteBacks[0];
|
|
146
|
+
expect(wb1.phase).toBe(WorkspaceWriteBackPhase.WORKSPACE_WRITE_BACK_PR_CREATED);
|
|
147
|
+
expect(wb1.branchName).toBe(SESSION_BRANCH);
|
|
148
|
+
expect(wb1.pullRequestNumber).toBe(5);
|
|
149
|
+
|
|
150
|
+
// The commit is on origin's session branch, authored as the agent, and
|
|
151
|
+
// main is untouched.
|
|
152
|
+
const tip1 = git(originDir, ["rev-parse", SESSION_BRANCH]).trim();
|
|
153
|
+
expect(git(originDir, ["show", `${tip1}:notes.md`])).toBe("approved content\n");
|
|
154
|
+
expect(git(originDir, ["log", "-1", "--format=%ae", SESSION_BRANCH]).trim())
|
|
155
|
+
.toBe(AGENT_GIT_AUTHOR_EMAIL);
|
|
156
|
+
expect(() => git(originDir, ["show", "main:notes.md"])).toThrow();
|
|
157
|
+
|
|
158
|
+
// ── Turn 2: a NEW coordinator (new execution, same session). The agent
|
|
159
|
+
// edited two files; the review discarded one — the reconcile snapped it
|
|
160
|
+
// back BEFORE finalize, so only the kept edit is in the tree. ──
|
|
161
|
+
writeFileSync(join(workDir, "notes.md"), "approved content\nkept edit\n");
|
|
162
|
+
writeFileSync(join(workDir, "rejected.md"), "discarded content\n");
|
|
163
|
+
rmSync(join(workDir, "rejected.md")); // the reconcile's snap-back
|
|
164
|
+
|
|
165
|
+
const status2 = create(AgentExecutionStatusSchema, {});
|
|
166
|
+
const coord2 = makeCoordinator("exec-turn-2", statusProtoWriter(status2));
|
|
167
|
+
await coord2.finalize();
|
|
168
|
+
|
|
169
|
+
const wb2 = status2.workspaceWriteBacks[0];
|
|
170
|
+
expect(wb2.phase).toBe(WorkspaceWriteBackPhase.WORKSPACE_WRITE_BACK_PR_CREATED);
|
|
171
|
+
expect(wb2.branchName, "same session branch across executions").toBe(SESSION_BRANCH);
|
|
172
|
+
expect(wb2.pullRequestNumber, "adopted, not re-created").toBe(5);
|
|
173
|
+
expect(apiCalls.filter((c) => c.method === "POST"), "exactly one PR ever created")
|
|
174
|
+
.toHaveLength(1);
|
|
175
|
+
|
|
176
|
+
// Both turns' commits are on the ONE session branch; the discarded file
|
|
177
|
+
// never reached the remote.
|
|
178
|
+
const tip2 = git(originDir, ["rev-parse", SESSION_BRANCH]).trim();
|
|
179
|
+
expect(git(originDir, ["show", `${tip2}:notes.md`])).toBe("approved content\nkept edit\n");
|
|
180
|
+
expect(() => git(originDir, ["show", `${tip2}:rejected.md`])).toThrow();
|
|
181
|
+
expect(
|
|
182
|
+
git(originDir, ["rev-list", "--count", `main..${SESSION_BRANCH}`]).trim(),
|
|
183
|
+
"one commit per approved turn",
|
|
184
|
+
).toBe("2");
|
|
185
|
+
|
|
186
|
+
// Each commit message carries its execution id for traceability.
|
|
187
|
+
const messages = git(originDir, ["log", "--format=%s", SESSION_BRANCH]);
|
|
188
|
+
expect(messages).toContain("agent changes (exec-turn-1)");
|
|
189
|
+
expect(messages).toContain("agent changes (exec-turn-2)");
|
|
190
|
+
},
|
|
191
|
+
GIT_TEST_TIMEOUT_MS,
|
|
192
|
+
);
|
|
193
|
+
|
|
194
|
+
it(
|
|
195
|
+
"recovers the session branch from the remote after a workspace re-provision",
|
|
196
|
+
async () => {
|
|
197
|
+
mockGithubApi();
|
|
198
|
+
|
|
199
|
+
// Turn 1 establishes the branch on origin.
|
|
200
|
+
writeFileSync(join(workDir, "notes.md"), "turn one\n");
|
|
201
|
+
const coord1 = makeCoordinator(
|
|
202
|
+
"exec-a",
|
|
203
|
+
statusProtoWriter(create(AgentExecutionStatusSchema, {})),
|
|
204
|
+
);
|
|
205
|
+
await coord1.finalize();
|
|
206
|
+
|
|
207
|
+
// The sandbox is torn down; a fresh clone knows nothing of the local
|
|
208
|
+
// branch (only origin has it).
|
|
209
|
+
rmSync(workDir, { recursive: true, force: true });
|
|
210
|
+
execFileSync("git", ["clone", originDir, workDir], { stdio: "pipe" });
|
|
211
|
+
expect(git(workDir, ["branch", "--show-current"]).trim()).toBe("main");
|
|
212
|
+
|
|
213
|
+
// Turn 2 in the fresh clone must continue the SAME branch, not fork a
|
|
214
|
+
// new one from main (which would orphan turn 1's commit).
|
|
215
|
+
writeFileSync(join(workDir, "more.md"), "turn two\n");
|
|
216
|
+
const status2 = create(AgentExecutionStatusSchema, {});
|
|
217
|
+
const coord2 = makeCoordinator("exec-b", statusProtoWriter(status2));
|
|
218
|
+
await coord2.finalize();
|
|
219
|
+
|
|
220
|
+
expect(status2.workspaceWriteBacks[0].branchName).toBe(SESSION_BRANCH);
|
|
221
|
+
const tip = git(originDir, ["rev-parse", SESSION_BRANCH]).trim();
|
|
222
|
+
expect(git(originDir, ["show", `${tip}:notes.md`]), "turn 1's commit is an ancestor")
|
|
223
|
+
.toBe("turn one\n");
|
|
224
|
+
expect(git(originDir, ["show", `${tip}:more.md`])).toBe("turn two\n");
|
|
225
|
+
expect(git(originDir, ["rev-list", "--count", `main..${SESSION_BRANCH}`]).trim())
|
|
226
|
+
.toBe("2");
|
|
227
|
+
},
|
|
228
|
+
GIT_TEST_TIMEOUT_MS,
|
|
229
|
+
);
|
|
230
|
+
});
|