@aviaratech/ai-delivery 0.1.0 → 0.2.1
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/CONTRIBUTING.md +1 -1
- package/README.md +124 -36
- package/dist/agent.d.ts +4 -1
- package/dist/agent.js +2 -1
- package/dist/agent.js.map +1 -1
- package/dist/cli.js +19 -5
- package/dist/cli.js.map +1 -1
- package/dist/config/deliveryConfig.d.ts +102 -4
- package/dist/config/deliveryConfig.js +120 -31
- package/dist/config/deliveryConfig.js.map +1 -1
- package/dist/config/deliveryConfig.test.js +9 -7
- package/dist/config/deliveryConfig.test.js.map +1 -1
- package/dist/dispatch.d.ts +1 -0
- package/dist/dispatch.js +50 -24
- package/dist/dispatch.js.map +1 -1
- package/dist/fixtures/discovery.d.ts +74 -0
- package/dist/fixtures/discovery.js +94 -0
- package/dist/fixtures/discovery.js.map +1 -0
- package/dist/git.d.ts +1 -1
- package/dist/git.js +13 -9
- package/dist/git.js.map +1 -1
- package/dist/github/client.d.ts +1 -1
- package/dist/github/commandIdentityPolicy.d.ts +1 -1
- package/dist/github/discovery.d.ts +22 -0
- package/dist/github/discovery.js +226 -0
- package/dist/github/discovery.js.map +1 -0
- package/dist/github/discovery.test.d.ts +1 -0
- package/dist/github/discovery.test.js +195 -0
- package/dist/github/discovery.test.js.map +1 -0
- package/dist/github/projectDelivery.d.ts +5 -0
- package/dist/github/projectDelivery.js +48 -8
- package/dist/github/projectDelivery.js.map +1 -1
- package/dist/github/repo.d.ts +3 -2
- package/dist/github/repo.js +24 -7
- package/dist/github/repo.js.map +1 -1
- package/dist/issue.d.ts +6 -1
- package/dist/issue.js +23 -3
- package/dist/issue.js.map +1 -1
- package/dist/lifecycle.test.js +416 -33
- package/dist/lifecycle.test.js.map +1 -1
- package/dist/mcp/index.js +6 -3
- package/dist/mcp/index.js.map +1 -1
- package/dist/mcp/tools.d.ts +1 -1
- package/dist/pr.js +25 -13
- package/dist/pr.js.map +1 -1
- package/dist/pr.test.js +3 -0
- package/dist/pr.test.js.map +1 -1
- package/dist/services/deliveryAdmission.d.ts +5 -2
- package/dist/services/deliveryAdmission.js +5 -4
- package/dist/services/deliveryAdmission.js.map +1 -1
- package/dist/verification.d.ts +3 -0
- package/dist/verification.js +153 -22
- package/dist/verification.js.map +1 -1
- package/dist/worktree.d.ts +4 -0
- package/dist/worktree.js +9 -7
- package/dist/worktree.js.map +1 -1
- package/package.json +1 -1
- package/plugins/ai-delivery/.claude-plugin/plugin.json +2 -2
package/dist/lifecycle.test.js
CHANGED
|
@@ -1,18 +1,21 @@
|
|
|
1
1
|
import assert from 'node:assert/strict';
|
|
2
|
-
import { execFileSync } from 'node:child_process';
|
|
2
|
+
import { execFileSync, spawn } from 'node:child_process';
|
|
3
3
|
import { createHash } from 'node:crypto';
|
|
4
4
|
import { cpSync, existsSync, mkdirSync, mkdtempSync, readFileSync, readlinkSync, readdirSync, realpathSync, rmSync, symlinkSync, unlinkSync, writeFileSync, } from 'node:fs';
|
|
5
5
|
import { tmpdir } from 'node:os';
|
|
6
6
|
import { dirname, join } from 'node:path';
|
|
7
|
-
import { test } from 'vitest';
|
|
7
|
+
import { test, vi } from 'vitest';
|
|
8
|
+
import { syntheticDiscoveryClients, syntheticOverrides } from './fixtures/discovery.js';
|
|
9
|
+
import { Octokit } from '@octokit/rest';
|
|
8
10
|
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
|
|
9
11
|
import { InMemoryTransport } from '@modelcontextprotocol/sdk/inMemory.js';
|
|
10
12
|
import { AI_DELIVERY_MCP_TOOLS } from './mcp/tools.js';
|
|
11
13
|
import { createAiDeliveryMcpServer } from './mcp/index.js';
|
|
12
|
-
import { loadDeliveryConfig } from './config/deliveryConfig.js';
|
|
14
|
+
import { loadDeliveryConfig, parseDeliveryConfig } from './config/deliveryConfig.js';
|
|
13
15
|
import { digestValue } from './delivery/index.js';
|
|
14
16
|
import { executeTool, resumedIssueUpdate, startTrackedIssue } from './dispatch.js';
|
|
15
|
-
import { gitCommonDir } from './git.js';
|
|
17
|
+
import { defaultBaseRef, gitCommonDir } from './git.js';
|
|
18
|
+
import * as githubClient from './github/client.js';
|
|
16
19
|
import { createIssue, developIssue, listIssueSubissues, readyCheck, resumeCreatedIssue, updateIssue, } from './issue.js';
|
|
17
20
|
import { checkoutPr, finishIssue, listPrs, mergePr, prChecks, publishPr, submitFormalReview } from './pr.js';
|
|
18
21
|
import { updateIssueWorktreeDelivery } from './services/worktreeRegistry.js';
|
|
@@ -20,6 +23,24 @@ import { assertDeliveryRuntimeAdmitted } from './services/deliveryAdmission.js';
|
|
|
20
23
|
import { getDeliveryRecords } from './services/deliveryRecordService.js';
|
|
21
24
|
import { createIssuePhaseEvidence, verifyIssue } from './verification.js';
|
|
22
25
|
import { cleanupNonIssueWorktree, prepareIssueWorktree, preparePrWorktree, prepareStandaloneWorktree, } from './worktree.js';
|
|
26
|
+
vi.mock('./github/client.js', async (importOriginal) => {
|
|
27
|
+
const actual = await importOriginal();
|
|
28
|
+
return {
|
|
29
|
+
...actual,
|
|
30
|
+
createDeliveryGitHubClients: async (input) => ({
|
|
31
|
+
...syntheticDiscoveryClients(),
|
|
32
|
+
authSource: 'app',
|
|
33
|
+
role: input.role,
|
|
34
|
+
rest: new Octokit({
|
|
35
|
+
request: {
|
|
36
|
+
fetch: async () => {
|
|
37
|
+
throw new Error('Unexpected network request in synthetic lifecycle test.');
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
}),
|
|
41
|
+
}),
|
|
42
|
+
};
|
|
43
|
+
});
|
|
23
44
|
function git(root, ...args) {
|
|
24
45
|
return execFileSync('git', args, { cwd: root, encoding: 'utf8' }).trim();
|
|
25
46
|
}
|
|
@@ -43,8 +64,9 @@ function directoryHash(path) {
|
|
|
43
64
|
visit(path, '');
|
|
44
65
|
return digestValue(entries);
|
|
45
66
|
}
|
|
46
|
-
function fixture(options = {}) {
|
|
67
|
+
async function fixture(options = {}) {
|
|
47
68
|
const root = realpathSync(mkdtempSync(join(tmpdir(), 'ai-delivery-lifecycle-')));
|
|
69
|
+
const remoteName = options.remote ?? 'origin';
|
|
48
70
|
const counter = join(root, '.git', 'stage-count.txt');
|
|
49
71
|
const secondCounter = join(root, '.git', 'second-stage-count.txt');
|
|
50
72
|
const failSecond = join(root, '.git', 'fail-second-stage');
|
|
@@ -53,7 +75,7 @@ function fixture(options = {}) {
|
|
|
53
75
|
git(root, 'config', 'user.name', 'Synthetic Delivery');
|
|
54
76
|
git(root, 'config', 'user.email', 'delivery@example.test');
|
|
55
77
|
git(root, 'init', '--bare', '-q', join(root, '.git', 'remote.git'));
|
|
56
|
-
git(root, 'remote', 'add',
|
|
78
|
+
git(root, 'remote', 'add', remoteName, join(root, '.git', 'remote.git'));
|
|
57
79
|
writeFileSync(join(root, '.gitignore'), '.issue-cli/\n.worktrees/\n');
|
|
58
80
|
writeFileSync(join(root, 'artifact.txt'), 'synthetic proof\n');
|
|
59
81
|
const config = {
|
|
@@ -95,9 +117,9 @@ function fixture(options = {}) {
|
|
|
95
117
|
identity: 'synthetic-reviewer',
|
|
96
118
|
},
|
|
97
119
|
},
|
|
98
|
-
schemaVersion: 'ai-delivery.config@
|
|
120
|
+
schemaVersion: 'ai-delivery.config@2',
|
|
99
121
|
};
|
|
100
|
-
writeFileSync(join(root, 'ai-delivery.config.json'), `${JSON.stringify(config)}\n`);
|
|
122
|
+
writeFileSync(join(root, 'ai-delivery.config.json'), `${JSON.stringify({ ...syntheticOverrides(parseDeliveryConfig(config)), ...(options.divergentOrigin ? { remote: remoteName } : {}) })}\n`);
|
|
101
123
|
writeFileSync(join(root, 'policy.mjs'), `
|
|
102
124
|
import { createHash } from 'node:crypto';
|
|
103
125
|
import { readFileSync } from 'node:fs';
|
|
@@ -109,6 +131,7 @@ const stable = value => Array.isArray(value) ? '[' + value.map(stable).join(',')
|
|
|
109
131
|
const digest = value => 'sha256:' + createHash('sha256').update(stable(value)).digest('hex');
|
|
110
132
|
const bytes = value => 'sha256:' + createHash('sha256').update(value).digest('hex');
|
|
111
133
|
const policyDigest = bytes(readFileSync(fileURLToPath(import.meta.url)));
|
|
134
|
+
export const deliverySettings = ${JSON.stringify({ roles: config.roles, commandPolicy: config.commandPolicy })};
|
|
112
135
|
export default {
|
|
113
136
|
schemaVersion: 'RepositoryDeliveryPolicy@1',
|
|
114
137
|
classifyExactRange(input) {
|
|
@@ -121,7 +144,7 @@ export default {
|
|
|
121
144
|
return { policyDigest, policyEvidence: { ...content, evidenceId: digest(content) },
|
|
122
145
|
requiredStages: [{ id: 'check', dependsOn: [], semanticInputKeys: ['source'],
|
|
123
146
|
resourceClass: 'source_only', commands: [{ label: 'count', argv: [process.execPath, '-e',
|
|
124
|
-
${JSON.stringify(`const fs=require('fs');const p=${JSON.stringify(counter)};fs.writeFileSync(p,String(Number(fs.existsSync(p)?fs.readFileSync(p,'utf8'):0)+1));`)}] }] }${options.twoStages
|
|
147
|
+
${JSON.stringify(options.firstStageScript ?? `const fs=require('fs');const p=${JSON.stringify(counter)};fs.writeFileSync(p,String(Number(fs.existsSync(p)?fs.readFileSync(p,'utf8'):0)+1));`)}] }] }${options.twoStages
|
|
125
148
|
? `, { id: 'second', dependsOn: ['check'], semanticInputKeys: ['source'],
|
|
126
149
|
resourceClass: 'source_only', commands: [{ label: 'retry', argv: [process.execPath, '-e',
|
|
127
150
|
${JSON.stringify(`const fs=require('fs');const p=${JSON.stringify(secondCounter)};fs.writeFileSync(p,String(Number(fs.existsSync(p)?fs.readFileSync(p,'utf8'):0)+1));if(fs.existsSync(${JSON.stringify(failSecond)}))process.exit(7);`)}] }] }`
|
|
@@ -140,8 +163,16 @@ export default {
|
|
|
140
163
|
`);
|
|
141
164
|
git(root, 'add', '.');
|
|
142
165
|
git(root, 'commit', '-qm', 'synthetic base');
|
|
143
|
-
git(root, 'push', '-q',
|
|
144
|
-
git(root, 'fetch', '-q',
|
|
166
|
+
git(root, 'push', '-q', remoteName, 'main');
|
|
167
|
+
git(root, 'fetch', '-q', remoteName, 'main');
|
|
168
|
+
git(root, 'config', `url.${join(root, '.git', 'remote.git')}.insteadOf`, 'https://github.com/example/widget.git');
|
|
169
|
+
git(root, 'remote', 'set-url', remoteName, 'https://github.com/example/widget.git');
|
|
170
|
+
if (options.divergentOrigin) {
|
|
171
|
+
git(root, 'remote', 'add', 'origin', 'https://github.com/other/unrelated.git');
|
|
172
|
+
const unrelated = git(root, 'commit-tree', 'HEAD^{tree}', '-m', 'Unrelated origin root');
|
|
173
|
+
git(root, 'update-ref', 'refs/remotes/origin/main', unrelated);
|
|
174
|
+
git(root, 'symbolic-ref', 'refs/remotes/origin/HEAD', 'refs/remotes/origin/main');
|
|
175
|
+
}
|
|
145
176
|
const runtimeRoot = join(root, '.git', 'synthetic-runtime');
|
|
146
177
|
const runtimeEntryPath = join(runtimeRoot, 'package', 'dist', 'cli.js');
|
|
147
178
|
const packagePath = join(runtimeRoot, 'package', 'package.json');
|
|
@@ -153,12 +184,12 @@ export default {
|
|
|
153
184
|
writeFileSync(runtimeEntryPath, 'synthetic CLI bytes\n');
|
|
154
185
|
writeFileSync(packagePath, JSON.stringify({ name: '@aviaratech/ai-delivery', version: '0.1.0' }));
|
|
155
186
|
writeFileSync(mcpLauncherPath, 'synthetic MCP launcher bytes\n');
|
|
156
|
-
writeFileSync(pluginManifestPath, JSON.stringify({ name: 'ai-delivery', packageVersion: '0.1.0', deliveryCapabilityVersion:
|
|
187
|
+
writeFileSync(pluginManifestPath, JSON.stringify({ name: 'ai-delivery', packageVersion: '0.1.0', deliveryCapabilityVersion: 2 }));
|
|
157
188
|
const content = {
|
|
158
|
-
capability: { cli:
|
|
189
|
+
capability: { cli: 2, mcp: 2 },
|
|
159
190
|
cliPath: runtimeEntryPath,
|
|
160
191
|
cliSha256: sha256(runtimeEntryPath),
|
|
161
|
-
configDigest: loadDeliveryConfig(root).configDigest,
|
|
192
|
+
configDigest: (await loadDeliveryConfig(root)).configDigest,
|
|
162
193
|
mcpLauncherPath,
|
|
163
194
|
mcpLauncherSha256: sha256(mcpLauncherPath),
|
|
164
195
|
packageVersion: '0.1.0',
|
|
@@ -167,7 +198,7 @@ export default {
|
|
|
167
198
|
pluginManifestPath,
|
|
168
199
|
pluginManifestSha256: sha256(pluginManifestPath),
|
|
169
200
|
repository: 'example/widget',
|
|
170
|
-
schemaVersion: 'ai-delivery.runtime-admission@
|
|
201
|
+
schemaVersion: 'ai-delivery.runtime-admission@2',
|
|
171
202
|
sourceArchiveSha256: digestValue('synthetic archive'),
|
|
172
203
|
sourceCommit: git(root, 'rev-parse', 'HEAD'),
|
|
173
204
|
};
|
|
@@ -182,15 +213,15 @@ export default {
|
|
|
182
213
|
}
|
|
183
214
|
}
|
|
184
215
|
test('installed CLI and MCP admission fails before a worktree mutation on source or capability drift', async () => {
|
|
185
|
-
const { root, runtimeEntryPath } = fixture();
|
|
216
|
+
const { root, runtimeEntryPath } = await fixture();
|
|
186
217
|
try {
|
|
187
|
-
const input = { repoRoot: root, runtimeEntryPath };
|
|
218
|
+
const input = { repoRoot: root, runtimeEntryPath, identity: 'synthetic-author' };
|
|
188
219
|
const path = join(root, '.git', 'ai-delivery', 'runtime-admission.json');
|
|
189
220
|
const original = readFileSync(path, 'utf8');
|
|
190
|
-
assert.equal(assertDeliveryRuntimeAdmitted(input).capability.mcp,
|
|
221
|
+
assert.equal((await assertDeliveryRuntimeAdmitted(input)).capability.mcp, 2);
|
|
191
222
|
const changedModule = join(dirname(runtimeEntryPath), 'mutated.js');
|
|
192
223
|
writeFileSync(changedModule, 'unadmitted module bytes\n');
|
|
193
|
-
assert.
|
|
224
|
+
await assert.rejects(assertDeliveryRuntimeAdmitted(input), /source, capability or repository admission/u);
|
|
194
225
|
rmSync(changedModule);
|
|
195
226
|
rmSync(path);
|
|
196
227
|
await assert.rejects(executeTool('issue_worktree_create', { name: 'scratch-admission', branch: 'scratch/admission' }, input), /runtime admission/u);
|
|
@@ -204,14 +235,122 @@ test('installed CLI and MCP admission fails before a worktree mutation on source
|
|
|
204
235
|
const { admissionId: _old, ...content } = admission;
|
|
205
236
|
const mismatched = { ...content, mcpLauncherSha256: sha256(launcherPath), capability: { cli: 1, mcp: 2 } };
|
|
206
237
|
writeFileSync(path, JSON.stringify({ ...mismatched, admissionId: digestValue(mismatched) }));
|
|
207
|
-
assert.
|
|
238
|
+
await assert.rejects(assertDeliveryRuntimeAdmitted(input), /source, capability or repository admission/u);
|
|
239
|
+
}
|
|
240
|
+
finally {
|
|
241
|
+
rmSync(root, { recursive: true, force: true });
|
|
242
|
+
}
|
|
243
|
+
});
|
|
244
|
+
test('effective policy settings drift invalidates admission even when the policy wrapper is unchanged', async () => {
|
|
245
|
+
const { root, runtimeEntryPath } = await fixture();
|
|
246
|
+
const policyPath = join(root, 'policy.mjs');
|
|
247
|
+
const authorEnv = 'AI_DELIVERY_TEST_DYNAMIC_AUTHOR';
|
|
248
|
+
const checkEnv = 'AI_DELIVERY_TEST_DYNAMIC_CHECK';
|
|
249
|
+
const previousAuthor = process.env[authorEnv];
|
|
250
|
+
const previousCheck = process.env[checkEnv];
|
|
251
|
+
try {
|
|
252
|
+
delete process.env[authorEnv];
|
|
253
|
+
delete process.env[checkEnv];
|
|
254
|
+
writeFileSync(policyPath, readFileSync(policyPath, 'utf8') +
|
|
255
|
+
`
|
|
256
|
+
Object.defineProperty(deliverySettings.roles.author, 'identity', { get: () => process.env.${authorEnv} ?? 'synthetic-author' });
|
|
257
|
+
Object.defineProperty(deliverySettings.commandPolicy.checks, 'test', { get: () => process.env.${checkEnv} ?? 'REQUIRED' });
|
|
258
|
+
`);
|
|
259
|
+
const admissionPath = join(root, '.git', 'ai-delivery', 'runtime-admission.json');
|
|
260
|
+
const { admissionId: _id, ...content } = JSON.parse(readFileSync(admissionPath, 'utf8'));
|
|
261
|
+
const baseline = await loadDeliveryConfig(root);
|
|
262
|
+
const admitted = { ...content, configDigest: baseline.configDigest };
|
|
263
|
+
writeFileSync(admissionPath, JSON.stringify({ ...admitted, admissionId: digestValue(admitted) }));
|
|
264
|
+
await assertDeliveryRuntimeAdmitted({ repoRoot: root, runtimeEntryPath });
|
|
265
|
+
const originalBytes = readFileSync(policyPath, 'utf8');
|
|
266
|
+
for (const [key, value] of [
|
|
267
|
+
[authorEnv, 'changed-author'],
|
|
268
|
+
[checkEnv, 'SKIP'],
|
|
269
|
+
]) {
|
|
270
|
+
process.env[key] = value;
|
|
271
|
+
assert.notEqual((await loadDeliveryConfig(root)).configDigest, baseline.configDigest);
|
|
272
|
+
await assert.rejects(assertDeliveryRuntimeAdmitted({ repoRoot: root, runtimeEntryPath }), /source, capability or repository admission/u);
|
|
273
|
+
delete process.env[key];
|
|
274
|
+
}
|
|
275
|
+
assert.equal(readFileSync(policyPath, 'utf8'), originalBytes);
|
|
276
|
+
}
|
|
277
|
+
finally {
|
|
278
|
+
if (previousAuthor === undefined)
|
|
279
|
+
delete process.env[authorEnv];
|
|
280
|
+
else
|
|
281
|
+
process.env[authorEnv] = previousAuthor;
|
|
282
|
+
if (previousCheck === undefined)
|
|
283
|
+
delete process.env[checkEnv];
|
|
284
|
+
else
|
|
285
|
+
process.env[checkEnv] = previousCheck;
|
|
286
|
+
rmSync(root, { recursive: true, force: true });
|
|
287
|
+
}
|
|
288
|
+
});
|
|
289
|
+
test('a changed discovered Project identity invalidates the installed admission', async () => {
|
|
290
|
+
const { root, runtimeEntryPath } = await fixture();
|
|
291
|
+
const base = syntheticDiscoveryClients();
|
|
292
|
+
const graphql = async (query, variables) => {
|
|
293
|
+
const result = await base.graphql(query, variables);
|
|
294
|
+
if (query.includes('query ProjectDeliveryConfiguration')) {
|
|
295
|
+
result.organization.projectV2.id = 'REPLACED-PROJECT';
|
|
296
|
+
}
|
|
297
|
+
return result;
|
|
298
|
+
};
|
|
299
|
+
const mocked = vi.spyOn(githubClient, 'createDeliveryGitHubClients').mockResolvedValueOnce({
|
|
300
|
+
authSource: 'app',
|
|
301
|
+
role: 'author',
|
|
302
|
+
rest: new Octokit(),
|
|
303
|
+
graphql: graphql,
|
|
304
|
+
});
|
|
305
|
+
try {
|
|
306
|
+
await assert.rejects(assertDeliveryRuntimeAdmitted({ repoRoot: root, runtimeEntryPath }), /source, capability or repository admission/u);
|
|
307
|
+
assert.equal(existsSync(join(root, '.worktrees')), false);
|
|
308
|
+
}
|
|
309
|
+
finally {
|
|
310
|
+
mocked.mockRestore();
|
|
311
|
+
rmSync(root, { recursive: true, force: true });
|
|
312
|
+
}
|
|
313
|
+
});
|
|
314
|
+
test('CLI dispatch and MCP reject ambiguous discovery with the same error before mutation', async () => {
|
|
315
|
+
const { root, runtimeEntryPath } = await fixture();
|
|
316
|
+
const path = join(root, 'ai-delivery.config.json');
|
|
317
|
+
const overrides = JSON.parse(readFileSync(path, 'utf8'));
|
|
318
|
+
delete overrides.project;
|
|
319
|
+
writeFileSync(path, JSON.stringify(overrides));
|
|
320
|
+
const base = syntheticDiscoveryClients();
|
|
321
|
+
const graphql = async (query, variables) => {
|
|
322
|
+
if (query.includes('projectsV2(first:'))
|
|
323
|
+
return { repository: { projectsV2: { nodes: [], pageInfo: { hasNextPage: false, endCursor: null } } } };
|
|
324
|
+
return base.graphql(query, variables);
|
|
325
|
+
};
|
|
326
|
+
const mocked = vi.spyOn(githubClient, 'createDeliveryGitHubClients').mockResolvedValue({
|
|
327
|
+
authSource: 'app',
|
|
328
|
+
role: 'author',
|
|
329
|
+
rest: new Octokit(),
|
|
330
|
+
graphql: graphql,
|
|
331
|
+
});
|
|
332
|
+
const execution = { repoRoot: root, runtimeEntryPath, identity: 'synthetic-author' };
|
|
333
|
+
const server = createAiDeliveryMcpServer(execution);
|
|
334
|
+
const client = new Client({ name: 'synthetic-discovery-client', version: '1.0.0' });
|
|
335
|
+
try {
|
|
336
|
+
await assert.rejects(executeTool('issue_create', { title: 'Ambiguous destination' }, execution), /Discovered 0 compatible linked Projects/u);
|
|
337
|
+
const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
|
|
338
|
+
await server.connect(serverTransport);
|
|
339
|
+
await client.connect(clientTransport);
|
|
340
|
+
const result = await client.callTool({ name: 'issue_create', arguments: { title: 'Ambiguous destination' } });
|
|
341
|
+
assert.equal(result.isError, true);
|
|
342
|
+
assert.match(JSON.stringify(result.content), /Discovered 0 compatible linked Projects/u);
|
|
343
|
+
assert.equal(existsSync(join(root, '.worktrees')), false);
|
|
208
344
|
}
|
|
209
345
|
finally {
|
|
346
|
+
await client.close();
|
|
347
|
+
await server.close();
|
|
348
|
+
mocked.mockRestore();
|
|
210
349
|
rmSync(root, { recursive: true, force: true });
|
|
211
350
|
}
|
|
212
351
|
});
|
|
213
352
|
test('an old active issue remains pinned while a new row uses the same canonical registry', async () => {
|
|
214
|
-
const { root } = fixture();
|
|
353
|
+
const { root } = await fixture();
|
|
215
354
|
try {
|
|
216
355
|
const oldPath = join(root, '.worktrees', 'issue-17');
|
|
217
356
|
git(root, 'worktree', 'add', '-b', 'issue/17', oldPath, 'main');
|
|
@@ -244,7 +383,7 @@ test('an old active issue remains pinned while a new row uses the same canonical
|
|
|
244
383
|
const relationshipContext = {
|
|
245
384
|
root,
|
|
246
385
|
repo: { owner: 'example', repo: 'widget' },
|
|
247
|
-
config: loadDeliveryConfig(root).config,
|
|
386
|
+
config: (await loadDeliveryConfig(root)).config,
|
|
248
387
|
clients: {
|
|
249
388
|
rest: {
|
|
250
389
|
issues: {
|
|
@@ -289,7 +428,7 @@ test('an old active issue remains pinned while a new row uses the same canonical
|
|
|
289
428
|
}
|
|
290
429
|
});
|
|
291
430
|
test('synthetic cutover restores exact pre-write state and resumes an incomplete verification stage', async () => {
|
|
292
|
-
const { root, counter, secondCounter, failSecond } = fixture({ twoStages: true });
|
|
431
|
+
const { root, counter, secondCounter, failSecond } = await fixture({ twoStages: true });
|
|
293
432
|
const backup = mkdtempSync(join(tmpdir(), 'ai-delivery-cutover-backup-'));
|
|
294
433
|
try {
|
|
295
434
|
const row = await prepareIssueWorktree({
|
|
@@ -313,19 +452,19 @@ test('synthetic cutover restores exact pre-write state and resumes an incomplete
|
|
|
313
452
|
const evidenceBefore = directoryHash(evidencePath);
|
|
314
453
|
const pointerBefore = readlinkSync(current);
|
|
315
454
|
const currentCli = join(current, 'package', 'dist', 'cli.js');
|
|
316
|
-
assert.equal(assertDeliveryRuntimeAdmitted({ repoRoot: root, runtimeEntryPath: currentCli }).capability.cli,
|
|
455
|
+
assert.equal((await assertDeliveryRuntimeAdmitted({ repoRoot: root, runtimeEntryPath: currentCli })).capability.cli, 2);
|
|
317
456
|
cpSync(oldRelease, newRelease, { recursive: true });
|
|
318
457
|
writeFileSync(join(newRelease, 'package', 'dist', 'cli.js'), 'unadmitted CLI bytes\n');
|
|
319
458
|
unlinkSync(current);
|
|
320
459
|
symlinkSync(newRelease, current);
|
|
321
|
-
await assert.rejects(executeTool('issue_worktree_create', { branch: 'scratch/cutover', name: 'scratch-cutover' }, { repoRoot: root, runtimeEntryPath: currentCli }), /source, capability or repository admission/u);
|
|
460
|
+
await assert.rejects(executeTool('issue_worktree_create', { branch: 'scratch/cutover', name: 'scratch-cutover' }, { repoRoot: root, runtimeEntryPath: currentCli, identity: 'synthetic-author' }), /source, capability or repository admission/u);
|
|
322
461
|
assert.equal(existsSync(join(root, '.worktrees', 'scratch-cutover')), false);
|
|
323
462
|
assert.equal(directoryHash(registryPath), registryBefore);
|
|
324
463
|
assert.equal(directoryHash(evidencePath), evidenceBefore);
|
|
325
464
|
unlinkSync(current);
|
|
326
465
|
symlinkSync(pointerBefore, current);
|
|
327
466
|
assert.equal(readlinkSync(current), pointerBefore);
|
|
328
|
-
assertDeliveryRuntimeAdmitted({ repoRoot: root, runtimeEntryPath: currentCli });
|
|
467
|
+
await assertDeliveryRuntimeAdmitted({ repoRoot: root, runtimeEntryPath: currentCli });
|
|
329
468
|
const uninterrupted = await verifyIssue({ issueNumber: 17, repoRoot: row.path });
|
|
330
469
|
assert.equal(readFileSync(counter, 'utf8'), '1');
|
|
331
470
|
assert.equal(readFileSync(secondCounter, 'utf8'), '1');
|
|
@@ -386,7 +525,7 @@ test('synthetic cutover restores exact pre-write state and resumes an incomplete
|
|
|
386
525
|
}
|
|
387
526
|
});
|
|
388
527
|
test('issue worktree verification resumes a completed stage and rejects changed inputs', async () => {
|
|
389
|
-
const { root, counter, runtimeEntryPath } = fixture();
|
|
528
|
+
const { root, counter, runtimeEntryPath } = await fixture();
|
|
390
529
|
try {
|
|
391
530
|
await assert.rejects(executeTool('issue_create', { title: 'Wrong repository', repo: 'other/widget' }, { repoRoot: root }), /Repository selector must match/u);
|
|
392
531
|
const row = await prepareIssueWorktree({
|
|
@@ -468,8 +607,242 @@ test('issue worktree verification resumes a completed stage and rejects changed
|
|
|
468
607
|
rmSync(root, { recursive: true, force: true });
|
|
469
608
|
}
|
|
470
609
|
});
|
|
610
|
+
test('a progressing stage remains observable beyond a former short deadline', async () => {
|
|
611
|
+
const marker = join(tmpdir(), `ai-delivery-stage-start-${process.pid}-${Date.now()}`);
|
|
612
|
+
const { root } = await fixture({
|
|
613
|
+
firstStageScript: `const fs=require('fs');fs.writeFileSync(${JSON.stringify(marker)},'started');process.stdout.write('begin');setTimeout(()=>{process.stdout.write('end');},180);`,
|
|
614
|
+
});
|
|
615
|
+
try {
|
|
616
|
+
const row = await prepareIssueWorktree({
|
|
617
|
+
baseRef: 'main',
|
|
618
|
+
identity: 'synthetic-author',
|
|
619
|
+
issueNumber: 17,
|
|
620
|
+
repoRoot: root,
|
|
621
|
+
});
|
|
622
|
+
writeFileSync(join(row.path, 'change.txt'), 'feature\n');
|
|
623
|
+
git(row.path, 'add', 'change.txt');
|
|
624
|
+
git(row.path, 'commit', '-qm', 'synthetic progressing stage');
|
|
625
|
+
const progressSpy = vi.spyOn(process.stderr, 'write');
|
|
626
|
+
let completed = false;
|
|
627
|
+
const run = verifyIssue({ issueNumber: 17, repoRoot: row.path }).then((value) => {
|
|
628
|
+
completed = true;
|
|
629
|
+
return value;
|
|
630
|
+
});
|
|
631
|
+
const markerDeadline = Date.now() + 2_000;
|
|
632
|
+
while (!existsSync(marker) && Date.now() < markerDeadline)
|
|
633
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
634
|
+
assert.equal(existsSync(marker), true);
|
|
635
|
+
assert.equal(completed, false, 'stage must remain observable while its process runs');
|
|
636
|
+
assert.ok(progressSpy.mock.calls.some(([chunk]) => String(chunk).includes('"state":"running"') && String(chunk).includes('"stageId":"check"')));
|
|
637
|
+
await new Promise((resolve) => setTimeout(resolve, 40));
|
|
638
|
+
assert.equal(completed, false, 'useful work continues beyond the injected former deadline');
|
|
639
|
+
const result = await run;
|
|
640
|
+
assert.equal(result.stageReceipts.length, 1);
|
|
641
|
+
}
|
|
642
|
+
finally {
|
|
643
|
+
vi.restoreAllMocks();
|
|
644
|
+
rmSync(marker, { force: true });
|
|
645
|
+
rmSync(root, { recursive: true, force: true });
|
|
646
|
+
}
|
|
647
|
+
});
|
|
648
|
+
test('cancelling verification stops its owned descendant and preserves an unrelated process', async () => {
|
|
649
|
+
const marker = join(tmpdir(), `ai-delivery-stage-child-${process.pid}-${Date.now()}`);
|
|
650
|
+
const heartbeat = `${marker}-heartbeat`;
|
|
651
|
+
const unrelatedHeartbeat = `${marker}-unrelated`;
|
|
652
|
+
const { root } = await fixture({
|
|
653
|
+
firstStageScript: `const fs=require('fs');const {spawn}=require('child_process');const child=spawn(process.execPath,['-e',${JSON.stringify(`const fs=require('fs');setInterval(()=>fs.appendFileSync(${JSON.stringify(heartbeat)},'x'),20);`)}],{stdio:'ignore'});child.unref();fs.writeFileSync(${JSON.stringify(marker)},String(child.pid));setTimeout(()=>{},400);`,
|
|
654
|
+
});
|
|
655
|
+
let descendantPid;
|
|
656
|
+
const unrelated = spawn(process.execPath, ['-e', `const fs=require('fs');setInterval(()=>fs.appendFileSync(${JSON.stringify(unrelatedHeartbeat)},'x'),20);`], {
|
|
657
|
+
detached: true,
|
|
658
|
+
stdio: 'ignore',
|
|
659
|
+
});
|
|
660
|
+
unrelated.unref();
|
|
661
|
+
try {
|
|
662
|
+
const row = await prepareIssueWorktree({
|
|
663
|
+
baseRef: 'main',
|
|
664
|
+
identity: 'synthetic-author',
|
|
665
|
+
issueNumber: 17,
|
|
666
|
+
repoRoot: root,
|
|
667
|
+
});
|
|
668
|
+
writeFileSync(join(row.path, 'change.txt'), 'feature\n');
|
|
669
|
+
git(row.path, 'add', 'change.txt');
|
|
670
|
+
git(row.path, 'commit', '-qm', 'synthetic cancellable stage');
|
|
671
|
+
const controller = new AbortController();
|
|
672
|
+
const run = verifyIssue({
|
|
673
|
+
issueNumber: 17,
|
|
674
|
+
repoRoot: row.path,
|
|
675
|
+
signal: controller.signal,
|
|
676
|
+
});
|
|
677
|
+
const markerDeadline = Date.now() + 2_000;
|
|
678
|
+
while (!existsSync(marker) && Date.now() < markerDeadline)
|
|
679
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
680
|
+
assert.equal(existsSync(marker), true);
|
|
681
|
+
descendantPid = Number(readFileSync(marker, 'utf8'));
|
|
682
|
+
controller.abort();
|
|
683
|
+
await assert.rejects(run, /cancelled/u);
|
|
684
|
+
assert.equal(existsSync(join(root, '.git', 'ai-delivery', 'verification@1', 'stages', 'check')), false);
|
|
685
|
+
await new Promise((resolve) => setTimeout(resolve, 60));
|
|
686
|
+
const stoppedSize = existsSync(heartbeat) ? readFileSync(heartbeat).length : 0;
|
|
687
|
+
const unrelatedSize = existsSync(unrelatedHeartbeat) ? readFileSync(unrelatedHeartbeat).length : 0;
|
|
688
|
+
await new Promise((resolve) => setTimeout(resolve, 80));
|
|
689
|
+
assert.equal(existsSync(heartbeat) ? readFileSync(heartbeat).length : 0, stoppedSize);
|
|
690
|
+
assert.ok(readFileSync(unrelatedHeartbeat).length > unrelatedSize);
|
|
691
|
+
}
|
|
692
|
+
finally {
|
|
693
|
+
if (descendantPid !== undefined) {
|
|
694
|
+
try {
|
|
695
|
+
process.kill(descendantPid, 'SIGKILL');
|
|
696
|
+
}
|
|
697
|
+
catch {
|
|
698
|
+
/* already stopped */
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
if (unrelated.pid !== undefined) {
|
|
702
|
+
try {
|
|
703
|
+
process.kill(unrelated.pid, 'SIGKILL');
|
|
704
|
+
}
|
|
705
|
+
catch {
|
|
706
|
+
/* already stopped */
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
rmSync(marker, { force: true });
|
|
710
|
+
rmSync(heartbeat, { force: true });
|
|
711
|
+
rmSync(unrelatedHeartbeat, { force: true });
|
|
712
|
+
rmSync(root, { recursive: true, force: true });
|
|
713
|
+
}
|
|
714
|
+
});
|
|
715
|
+
test.each([
|
|
716
|
+
{ name: 'nonzero exit', terminate: 'process.exit(7)', expected: /exit 7/u },
|
|
717
|
+
{ name: 'signal', terminate: "process.kill(process.pid,'SIGTERM')", expected: /signal SIGTERM/u },
|
|
718
|
+
])('a stage $name stops descendants holding its output pipes open', async ({ terminate, expected }) => {
|
|
719
|
+
const marker = join(tmpdir(), `ai-delivery-failed-stage-${process.pid}-${Date.now()}`);
|
|
720
|
+
const heartbeat = `${marker}-heartbeat`;
|
|
721
|
+
const { root } = await fixture({
|
|
722
|
+
firstStageScript: `const fs=require('fs');const {spawn}=require('child_process');const child=spawn(process.execPath,['-e',${JSON.stringify(`const fs=require('fs');setInterval(()=>fs.appendFileSync(${JSON.stringify(heartbeat)},'x'),20);`)}],{stdio:['ignore','inherit','inherit']});child.unref();fs.writeFileSync(${JSON.stringify(marker)},String(child.pid));${terminate};`,
|
|
723
|
+
});
|
|
724
|
+
let descendantPid;
|
|
725
|
+
let timeout;
|
|
726
|
+
try {
|
|
727
|
+
const row = await prepareIssueWorktree({
|
|
728
|
+
baseRef: 'main',
|
|
729
|
+
identity: 'synthetic-author',
|
|
730
|
+
issueNumber: 17,
|
|
731
|
+
repoRoot: root,
|
|
732
|
+
});
|
|
733
|
+
writeFileSync(join(row.path, 'change.txt'), 'feature\n');
|
|
734
|
+
git(row.path, 'add', 'change.txt');
|
|
735
|
+
git(row.path, 'commit', '-qm', 'synthetic failing stage');
|
|
736
|
+
const controller = new AbortController();
|
|
737
|
+
const run = verifyIssue({ issueNumber: 17, repoRoot: row.path, signal: controller.signal });
|
|
738
|
+
const rejection = assert.rejects(run, expected);
|
|
739
|
+
const markerDeadline = Date.now() + 2_000;
|
|
740
|
+
while (!existsSync(marker) && Date.now() < markerDeadline)
|
|
741
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
742
|
+
assert.equal(existsSync(marker), true);
|
|
743
|
+
descendantPid = Number(readFileSync(marker, 'utf8'));
|
|
744
|
+
timeout = setTimeout(() => controller.abort(), 500);
|
|
745
|
+
await rejection;
|
|
746
|
+
assert.equal(existsSync(join(root, '.git', 'ai-delivery', 'verification@1', 'stages', 'check')), false);
|
|
747
|
+
await new Promise((resolve) => setTimeout(resolve, 60));
|
|
748
|
+
const stoppedSize = existsSync(heartbeat) ? readFileSync(heartbeat).length : 0;
|
|
749
|
+
await new Promise((resolve) => setTimeout(resolve, 80));
|
|
750
|
+
assert.equal(existsSync(heartbeat) ? readFileSync(heartbeat).length : 0, stoppedSize);
|
|
751
|
+
}
|
|
752
|
+
finally {
|
|
753
|
+
if (timeout !== undefined)
|
|
754
|
+
clearTimeout(timeout);
|
|
755
|
+
if (descendantPid !== undefined) {
|
|
756
|
+
try {
|
|
757
|
+
process.kill(descendantPid, 'SIGKILL');
|
|
758
|
+
}
|
|
759
|
+
catch {
|
|
760
|
+
/* already stopped */
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
rmSync(marker, { force: true });
|
|
764
|
+
rmSync(heartbeat, { force: true });
|
|
765
|
+
rmSync(root, { recursive: true, force: true });
|
|
766
|
+
}
|
|
767
|
+
});
|
|
768
|
+
test('MCP request cancellation stops verification and leaves no stage receipt', async () => {
|
|
769
|
+
const marker = join(tmpdir(), `ai-delivery-mcp-cancel-${process.pid}-${Date.now()}`);
|
|
770
|
+
const heartbeat = `${marker}-heartbeat`;
|
|
771
|
+
const { root, runtimeEntryPath } = await fixture({
|
|
772
|
+
firstStageScript: `const fs=require('fs');const {spawn}=require('child_process');const child=spawn(process.execPath,['-e',${JSON.stringify(`const fs=require('fs');setInterval(()=>fs.appendFileSync(${JSON.stringify(heartbeat)},'x'),20);`)}],{stdio:'ignore'});child.unref();fs.writeFileSync(${JSON.stringify(marker)},String(child.pid));setTimeout(()=>{},1000);`,
|
|
773
|
+
});
|
|
774
|
+
const server = createAiDeliveryMcpServer({ repoRoot: root, runtimeEntryPath, identity: 'synthetic-author' });
|
|
775
|
+
const client = new Client({ name: 'synthetic-cancelling-client', version: '1.0.0' });
|
|
776
|
+
const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
|
|
777
|
+
let descendantPid;
|
|
778
|
+
try {
|
|
779
|
+
const row = await prepareIssueWorktree({
|
|
780
|
+
baseRef: 'main',
|
|
781
|
+
identity: 'synthetic-author',
|
|
782
|
+
issueNumber: 17,
|
|
783
|
+
repoRoot: root,
|
|
784
|
+
});
|
|
785
|
+
writeFileSync(join(row.path, 'change.txt'), 'feature\n');
|
|
786
|
+
git(row.path, 'add', 'change.txt');
|
|
787
|
+
git(row.path, 'commit', '-qm', 'synthetic MCP cancellable stage');
|
|
788
|
+
await server.connect(serverTransport);
|
|
789
|
+
await client.connect(clientTransport);
|
|
790
|
+
const controller = new AbortController();
|
|
791
|
+
const call = client.callTool({ name: 'issue_verify', arguments: { issueNumber: 17 } }, undefined, {
|
|
792
|
+
signal: controller.signal,
|
|
793
|
+
});
|
|
794
|
+
const markerDeadline = Date.now() + 2_000;
|
|
795
|
+
while (!existsSync(marker) && Date.now() < markerDeadline)
|
|
796
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
797
|
+
assert.equal(existsSync(marker), true);
|
|
798
|
+
descendantPid = Number(readFileSync(marker, 'utf8'));
|
|
799
|
+
controller.abort();
|
|
800
|
+
await assert.rejects(call, /abort/iu);
|
|
801
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
802
|
+
const stoppedSize = existsSync(heartbeat) ? readFileSync(heartbeat).length : 0;
|
|
803
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
804
|
+
assert.equal(existsSync(heartbeat) ? readFileSync(heartbeat).length : 0, stoppedSize);
|
|
805
|
+
assert.equal(existsSync(join(root, '.git', 'ai-delivery', 'verification@1', 'stages', 'check')), false);
|
|
806
|
+
}
|
|
807
|
+
finally {
|
|
808
|
+
await client.close();
|
|
809
|
+
await server.close();
|
|
810
|
+
if (descendantPid !== undefined) {
|
|
811
|
+
try {
|
|
812
|
+
process.kill(descendantPid, 'SIGKILL');
|
|
813
|
+
}
|
|
814
|
+
catch {
|
|
815
|
+
/* already stopped */
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
rmSync(marker, { force: true });
|
|
819
|
+
rmSync(heartbeat, { force: true });
|
|
820
|
+
rmSync(root, { recursive: true, force: true });
|
|
821
|
+
}
|
|
822
|
+
});
|
|
823
|
+
test('the stage output bound covers both streams and publishes no checkpoint on overflow', async () => {
|
|
824
|
+
const { root } = await fixture({
|
|
825
|
+
firstStageScript: `process.stdout.write(Buffer.alloc(4*1024*1024));process.stderr.write(Buffer.alloc(4*1024*1024+1));`,
|
|
826
|
+
});
|
|
827
|
+
try {
|
|
828
|
+
const row = await prepareIssueWorktree({
|
|
829
|
+
baseRef: 'main',
|
|
830
|
+
identity: 'synthetic-author',
|
|
831
|
+
issueNumber: 17,
|
|
832
|
+
repoRoot: root,
|
|
833
|
+
});
|
|
834
|
+
writeFileSync(join(row.path, 'change.txt'), 'feature\n');
|
|
835
|
+
git(row.path, 'add', 'change.txt');
|
|
836
|
+
git(row.path, 'commit', '-qm', 'synthetic output bound');
|
|
837
|
+
await assert.rejects(verifyIssue({ issueNumber: 17, repoRoot: row.path }), /captured output exceeded 8 MiB/u);
|
|
838
|
+
assert.equal(existsSync(join(root, '.git', 'ai-delivery', 'verification@1', 'stages', 'check')), false);
|
|
839
|
+
}
|
|
840
|
+
finally {
|
|
841
|
+
rmSync(root, { recursive: true, force: true });
|
|
842
|
+
}
|
|
843
|
+
});
|
|
471
844
|
test('scratch start derives safe names and reports the requested worktree', async () => {
|
|
472
|
-
const { root, runtimeEntryPath } = fixture();
|
|
845
|
+
const { root, runtimeEntryPath } = await fixture();
|
|
473
846
|
try {
|
|
474
847
|
const first = (await executeTool('issue_start', { scratch: true, request: 'Investigate button layout' }, { repoRoot: root, runtimeEntryPath, identity: 'synthetic-author' }));
|
|
475
848
|
assert.equal(first.mode, 'scratch');
|
|
@@ -543,10 +916,17 @@ test('MCP request rejects unsupported legacy options before lifecycle dispatch',
|
|
|
543
916
|
await server.close();
|
|
544
917
|
}
|
|
545
918
|
});
|
|
546
|
-
test(
|
|
547
|
-
|
|
919
|
+
test.each([
|
|
920
|
+
{ remote: 'origin', divergentOrigin: false },
|
|
921
|
+
{ remote: 'upstream', divergentOrigin: false },
|
|
922
|
+
{ remote: 'upstream', divergentOrigin: true },
|
|
923
|
+
])('synthetic lifecycle finishes with remote $remote and divergent origin $divergentOrigin', async (routing) => {
|
|
924
|
+
const { root } = await fixture(routing);
|
|
925
|
+
const remoteName = routing.remote;
|
|
926
|
+
assert.equal(defaultBaseRef(root, remoteName), `refs/remotes/${remoteName}/main`);
|
|
548
927
|
const remote = join(root, '.git', 'remote.git');
|
|
549
|
-
const
|
|
928
|
+
const configuration = await loadDeliveryConfig(root);
|
|
929
|
+
const config = configuration.config;
|
|
550
930
|
const baseSha = git(root, 'rev-parse', 'main');
|
|
551
931
|
const issueNumber = 17;
|
|
552
932
|
const prNumber = 23;
|
|
@@ -639,6 +1019,7 @@ test('synthetic issue traverses native intake, readiness, receipt-bound App revi
|
|
|
639
1019
|
options: [],
|
|
640
1020
|
issueField: {
|
|
641
1021
|
__typename: 'IssueFieldSingleSelect',
|
|
1022
|
+
id: 'ISSUE-FIELD-101',
|
|
642
1023
|
fullDatabaseId: '101',
|
|
643
1024
|
name: 'Estimate',
|
|
644
1025
|
options: ['1', '2', '4'].map((name) => ({ id: name, name })),
|
|
@@ -652,6 +1033,7 @@ test('synthetic issue traverses native intake, readiness, receipt-bound App revi
|
|
|
652
1033
|
options: [],
|
|
653
1034
|
issueField: {
|
|
654
1035
|
__typename: 'IssueFieldSingleSelect',
|
|
1036
|
+
id: 'ISSUE-FIELD-102',
|
|
655
1037
|
fullDatabaseId: '102',
|
|
656
1038
|
name: 'Urgency',
|
|
657
1039
|
options: ['High', 'Low'].map((name) => ({ id: name, name })),
|
|
@@ -853,7 +1235,7 @@ test('synthetic issue traverses native intake, readiness, receipt-bound App revi
|
|
|
853
1235
|
createCommit: async (input) => {
|
|
854
1236
|
calls.push('git:createCommit');
|
|
855
1237
|
mergeSha = git(root, 'commit-tree', input.tree, '-p', input.parents[0], '-p', input.parents[1], '-m', input.message);
|
|
856
|
-
git(root, 'push', '-q',
|
|
1238
|
+
git(root, 'push', '-q', remoteName, `${mergeSha}:refs/heads/merge-object`);
|
|
857
1239
|
return { data: { sha: mergeSha } };
|
|
858
1240
|
},
|
|
859
1241
|
getCommit: async () => ({
|
|
@@ -881,6 +1263,7 @@ test('synthetic issue traverses native intake, readiness, receipt-bound App revi
|
|
|
881
1263
|
root,
|
|
882
1264
|
repo: { owner: 'example', repo: 'widget' },
|
|
883
1265
|
config,
|
|
1266
|
+
configuration,
|
|
884
1267
|
clients: {
|
|
885
1268
|
authSource: 'app',
|
|
886
1269
|
role: 'author',
|
|
@@ -967,7 +1350,7 @@ test('synthetic issue traverses native intake, readiness, receipt-bound App revi
|
|
|
967
1350
|
await assert.rejects(publishPr(context, { issueNumber }), /Git push requires the selected author GitHub App installation token/);
|
|
968
1351
|
// The public push requires a real author App installation token. Bind a synthetic
|
|
969
1352
|
// publication at that external boundary so the remaining public phases run offline.
|
|
970
|
-
git(row.path, 'push', '-q',
|
|
1353
|
+
git(row.path, 'push', '-q', remoteName, `${headSha}:refs/heads/issue/17`);
|
|
971
1354
|
const publishEvidence = await createIssuePhaseEvidence({ issueNumber, phase: 'publish', repoRoot: row.path });
|
|
972
1355
|
const publicationContent = {
|
|
973
1356
|
baseSha,
|