@aviaratech/ai-delivery 0.3.4 → 0.3.6
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/README.md +135 -20
- package/dist/agent.d.ts +2 -0
- package/dist/agent.js +1 -0
- package/dist/agent.js.map +1 -1
- package/dist/cli.js +77 -2
- package/dist/cli.js.map +1 -1
- package/dist/dispatch.js +39 -1
- package/dist/dispatch.js.map +1 -1
- package/dist/github/client.d.ts +1 -0
- package/dist/github/client.js +4 -1
- package/dist/github/client.js.map +1 -1
- package/dist/github/commandIdentityPolicy.js +3 -0
- package/dist/github/commandIdentityPolicy.js.map +1 -1
- package/dist/issue.d.ts +1 -0
- package/dist/issue.js +24 -10
- package/dist/issue.js.map +1 -1
- package/dist/lifecycle.test.js +496 -30
- package/dist/lifecycle.test.js.map +1 -1
- package/dist/mcp/index.js +1 -1
- package/dist/mcp/tools.d.ts +63 -0
- package/dist/mcp/tools.js +66 -0
- package/dist/mcp/tools.js.map +1 -1
- package/dist/pr.d.ts +4 -4
- package/dist/pr.js +10 -6
- package/dist/pr.js.map +1 -1
- package/dist/review.test.js +163 -0
- package/dist/review.test.js.map +1 -1
- package/dist/services/deliveryAdmission.d.ts +13 -2
- package/dist/services/deliveryAdmission.js +41 -2
- package/dist/services/deliveryAdmission.js.map +1 -1
- package/dist/services/worktreeRegistry.d.ts +12 -0
- package/dist/services/worktreeRegistry.js +146 -4
- package/dist/services/worktreeRegistry.js.map +1 -1
- package/dist/setup.d.ts +58 -0
- package/dist/setup.js +415 -0
- package/dist/setup.js.map +1 -0
- package/dist/setup.test.d.ts +1 -0
- package/dist/setup.test.js +744 -0
- package/dist/setup.test.js.map +1 -0
- package/dist/verification.d.ts +37 -0
- package/dist/verification.js +214 -15
- package/dist/verification.js.map +1 -1
- package/dist/worktree.d.ts +3 -0
- package/dist/worktree.js +38 -8
- package/dist/worktree.js.map +1 -1
- package/dist/worktreeTransition.d.ts +173 -0
- package/dist/worktreeTransition.js +1012 -0
- package/dist/worktreeTransition.js.map +1 -0
- package/dist/worktreeTransition.test.d.ts +1 -0
- package/dist/worktreeTransition.test.js +1179 -0
- package/dist/worktreeTransition.test.js.map +1 -0
- package/package.json +1 -1
- package/plugins/ai-delivery/.claude-plugin/plugin.json +1 -1
- package/plugins/ai-delivery/README.md +1 -1
- package/plugins/ai-delivery/skills/worktree-lifecycle/SKILL.md +9 -0
package/dist/lifecycle.test.js
CHANGED
|
@@ -13,7 +13,7 @@ import { AI_DELIVERY_MCP_TOOLS } from './mcp/tools.js';
|
|
|
13
13
|
import { createAiDeliveryMcpServer } from './mcp/index.js';
|
|
14
14
|
import { loadDeliveryConfig, parseDeliveryConfig } from './config/deliveryConfig.js';
|
|
15
15
|
import { digestValue } from './delivery/index.js';
|
|
16
|
-
import { executeTool, resumedIssueUpdate, startTrackedIssue } from './dispatch.js';
|
|
16
|
+
import { contextFor, executeTool, resumedIssueUpdate, startTrackedIssue } from './dispatch.js';
|
|
17
17
|
import { defaultBaseRef, gitCommonDir } from './git.js';
|
|
18
18
|
import * as githubClient from './github/client.js';
|
|
19
19
|
import * as atomicJson from './utils/atomicJson.js';
|
|
@@ -329,7 +329,7 @@ export default {
|
|
|
329
329
|
mkdirSync(dirname(path), { recursive: true });
|
|
330
330
|
}
|
|
331
331
|
writeFileSync(runtimeEntryPath, 'synthetic CLI bytes\n');
|
|
332
|
-
writeFileSync(packagePath, JSON.stringify({ name: '@aviaratech/ai-delivery', version: '0.1.0' }));
|
|
332
|
+
writeFileSync(packagePath, JSON.stringify({ name: '@aviaratech/ai-delivery', version: '0.1.0', bin: { 'ai-delivery': './dist/cli.js' } }));
|
|
333
333
|
writeFileSync(mcpLauncherPath, 'synthetic MCP launcher bytes\n');
|
|
334
334
|
writeFileSync(pluginManifestPath, JSON.stringify({ name: 'ai-delivery', packageVersion: '0.1.0', deliveryCapabilityVersion: 2 }));
|
|
335
335
|
const content = {
|
|
@@ -351,7 +351,8 @@ export default {
|
|
|
351
351
|
};
|
|
352
352
|
const admissionPath = join(root, '.git', 'ai-delivery', 'runtime-admission.json');
|
|
353
353
|
mkdirSync(dirname(admissionPath), { recursive: true });
|
|
354
|
-
|
|
354
|
+
if (!options.omitRuntimeAdmission)
|
|
355
|
+
writeFileSync(admissionPath, JSON.stringify({ ...content, admissionId: digestValue(content) }), { mode: 0o600 });
|
|
355
356
|
return { root, counter, secondCounter, failSecond, runtimeEntryPath };
|
|
356
357
|
}
|
|
357
358
|
catch (error) {
|
|
@@ -1879,6 +1880,37 @@ test('filesystem output from a sampled detached child crosses its declared root
|
|
|
1879
1880
|
rmSync(root, { recursive: true, force: true });
|
|
1880
1881
|
}
|
|
1881
1882
|
});
|
|
1883
|
+
test('contained npm bin aliases are counted once and target growth still enforces the output bound', async () => {
|
|
1884
|
+
const outside = realpathSync(mkdtempSync(join(tmpdir(), 'ai-delivery-contained-alias-')));
|
|
1885
|
+
const target = join(outside, 'output', 'actual');
|
|
1886
|
+
const { root } = await fixture({
|
|
1887
|
+
firstStageScript: `require('fs').appendFileSync(${JSON.stringify(target)},'x'.repeat(512));`,
|
|
1888
|
+
});
|
|
1889
|
+
try {
|
|
1890
|
+
mkdirSync(dirname(target));
|
|
1891
|
+
writeFileSync(target, 'baseline');
|
|
1892
|
+
symlinkSync('./actual', join(dirname(target), 'alias'));
|
|
1893
|
+
const row = await prepareIssueWorktree({
|
|
1894
|
+
baseRef: 'main',
|
|
1895
|
+
identity: 'synthetic-author',
|
|
1896
|
+
issueNumber: 17,
|
|
1897
|
+
repoRoot: root,
|
|
1898
|
+
});
|
|
1899
|
+
const bounds = {
|
|
1900
|
+
maxAggregateRssBytes: 1_000_000_000,
|
|
1901
|
+
minFreeDiskBytes: 1,
|
|
1902
|
+
maxNewOutputBytes: 600,
|
|
1903
|
+
outputRoots: [dirname(target)],
|
|
1904
|
+
};
|
|
1905
|
+
const run = await verifyIssue({ issueNumber: 17, repoRoot: row.path, resourceBounds: bounds });
|
|
1906
|
+
assert.equal(run.resources?.maxSampledNewOutputBytes, 512);
|
|
1907
|
+
await assert.rejects(verifyIssue({ issueNumber: 17, repoRoot: row.path, resourceBounds: { ...bounds, maxNewOutputBytes: 100 } }), /filesystem output.*exceeded/u);
|
|
1908
|
+
}
|
|
1909
|
+
finally {
|
|
1910
|
+
rmSync(root, { recursive: true, force: true });
|
|
1911
|
+
rmSync(outside, { recursive: true, force: true });
|
|
1912
|
+
}
|
|
1913
|
+
});
|
|
1882
1914
|
test('a symbolic link in declared output roots fails before publishing sampled output evidence', async () => {
|
|
1883
1915
|
const { root } = await fixture();
|
|
1884
1916
|
const outside = mkdtempSync(join(tmpdir(), 'ai-delivery-output-link-'));
|
|
@@ -2525,8 +2557,228 @@ test('scratch start derives safe names and reports the requested worktree', asyn
|
|
|
2525
2557
|
rmSync(root, { recursive: true, force: true });
|
|
2526
2558
|
}
|
|
2527
2559
|
});
|
|
2560
|
+
test.each(['direct-develop', 'direct-start', 'develop', 'existing-start', 'new-start', 'scratch', 'standalone'])('development access preflight rejects absent reviewer credentials before %s preparation', async (entry) => {
|
|
2561
|
+
const { root, runtimeEntryPath } = await fixture();
|
|
2562
|
+
const admissionPath = join(root, '.git', 'ai-delivery', 'runtime-admission.json');
|
|
2563
|
+
const admissionBefore = readFileSync(admissionPath, 'utf8');
|
|
2564
|
+
const syntheticClients = githubClient.createDeliveryGitHubClients;
|
|
2565
|
+
const actual = await vi.importActual('./github/client.js');
|
|
2566
|
+
const mocked = vi
|
|
2567
|
+
.spyOn(githubClient, 'createDeliveryGitHubClients')
|
|
2568
|
+
.mockImplementation((input) => input.role === 'reviewer' ? actual.createDeliveryGitHubClients({ ...input, env: {} }) : syntheticClients(input));
|
|
2569
|
+
const execution = { repoRoot: root, runtimeEntryPath, identity: 'synthetic-author' };
|
|
2570
|
+
const tracking = {
|
|
2571
|
+
title: 'Synthetic change',
|
|
2572
|
+
body: '## Outcome\nDeliver a verified change.\n\n## Scope\n- `artifact.txt`\n\n## Acceptance Criteria\n- [ ] Change is verified\n- [ ] Review is recorded\n\n## Verification\nRun `node --version`.',
|
|
2573
|
+
points: 2,
|
|
2574
|
+
develop: true,
|
|
2575
|
+
};
|
|
2576
|
+
try {
|
|
2577
|
+
const run = async () => {
|
|
2578
|
+
if (entry === 'direct-develop' || entry === 'direct-start') {
|
|
2579
|
+
const context = await contextFor(execution, 'develop');
|
|
2580
|
+
return entry === 'direct-develop' ? developIssue(context, 17) : startTrackedIssue(context, tracking);
|
|
2581
|
+
}
|
|
2582
|
+
if (entry === 'develop')
|
|
2583
|
+
return executeTool('issue_develop', { issueNumber: 17 }, execution);
|
|
2584
|
+
if (entry === 'existing-start')
|
|
2585
|
+
return executeTool('issue_start', { issueNumber: 17 }, execution);
|
|
2586
|
+
if (entry === 'new-start')
|
|
2587
|
+
return executeTool('issue_start', tracking, execution);
|
|
2588
|
+
if (entry === 'scratch')
|
|
2589
|
+
return executeTool('issue_start', { scratch: true, request: 'Inspect parser' }, execution);
|
|
2590
|
+
return executeTool('issue_worktree_create', { name: 'scratch-access', branch: 'scratch/access' }, execution);
|
|
2591
|
+
};
|
|
2592
|
+
await assert.rejects(run(), /Missing GitHub App credentials for reviewer role/u);
|
|
2593
|
+
assert.equal(existsSync(join(root, '.worktrees')), false);
|
|
2594
|
+
assert.equal(existsSync(join(root, '.issue-cli', 'worktrees.json')), false);
|
|
2595
|
+
assert.equal(readFileSync(admissionPath, 'utf8'), admissionBefore);
|
|
2596
|
+
}
|
|
2597
|
+
finally {
|
|
2598
|
+
mocked.mockRestore();
|
|
2599
|
+
rmSync(root, { recursive: true, force: true });
|
|
2600
|
+
}
|
|
2601
|
+
});
|
|
2602
|
+
test.each(['author-source', 'author-readback', 'same-actor', 'reviewer-repository'])('development access preflight rejects a mismatched %s route before issue reads or writes', async (failure) => {
|
|
2603
|
+
const { root, runtimeEntryPath } = await fixture({ personalAuthor: true });
|
|
2604
|
+
const syntheticClients = githubClient.createDeliveryGitHubClients;
|
|
2605
|
+
const mocked = vi.spyOn(githubClient, 'createDeliveryGitHubClients').mockImplementation(async (input) => {
|
|
2606
|
+
const clients = await syntheticClients(input);
|
|
2607
|
+
if (input.role === 'author' && failure === 'author-source')
|
|
2608
|
+
return { ...clients, authSource: 'app' };
|
|
2609
|
+
if (input.role === 'author' && failure === 'author-readback')
|
|
2610
|
+
return { ...clients, authenticatedAuthor: async () => ({ actorLogin: '', credentialIdentity: '' }) };
|
|
2611
|
+
if (input.role === 'reviewer' && failure === 'same-actor')
|
|
2612
|
+
return { ...clients, appActorLogin: async () => 'host-user' };
|
|
2613
|
+
if (input.role === 'reviewer' && failure === 'reviewer-repository')
|
|
2614
|
+
Object.assign(clients.rest.repos, { get: async () => ({ data: { full_name: 'example/other' } }) });
|
|
2615
|
+
return clients;
|
|
2616
|
+
});
|
|
2617
|
+
try {
|
|
2618
|
+
const context = await contextFor({ repoRoot: root, runtimeEntryPath, identity: 'host-author' }, 'develop');
|
|
2619
|
+
await assert.rejects(developIssue(context, 17), {
|
|
2620
|
+
'author-source': /requires the configured author credential/u,
|
|
2621
|
+
'author-readback': /Author GitHub identity readback is incomplete/u,
|
|
2622
|
+
'same-actor': /same GitHub actor/u,
|
|
2623
|
+
'reviewer-repository': /repository readback disagrees/u,
|
|
2624
|
+
}[failure]);
|
|
2625
|
+
assert.equal(existsSync(join(root, '.worktrees')), false);
|
|
2626
|
+
assert.equal(existsSync(join(root, '.git', 'ai-delivery', 'issue-info')), false);
|
|
2627
|
+
}
|
|
2628
|
+
finally {
|
|
2629
|
+
mocked.mockRestore();
|
|
2630
|
+
rmSync(root, { recursive: true, force: true });
|
|
2631
|
+
}
|
|
2632
|
+
});
|
|
2633
|
+
test('development access preflight permits the configured host author and independent App with unreadable rules', async () => {
|
|
2634
|
+
const { root, runtimeEntryPath } = await fixture({ personalAuthor: true });
|
|
2635
|
+
const admissionPath = join(root, '.git', 'ai-delivery', 'runtime-admission.json');
|
|
2636
|
+
const admissionBefore = readFileSync(admissionPath, 'utf8');
|
|
2637
|
+
try {
|
|
2638
|
+
const row = (await executeTool('issue_worktree_create', { name: 'scratch-host', branch: 'scratch/host' }, { repoRoot: root, runtimeEntryPath, identity: 'host-author' }));
|
|
2639
|
+
assert.equal(row.identity, 'host-author');
|
|
2640
|
+
assert.equal(row.path, join(root, '.worktrees', 'scratch-host'));
|
|
2641
|
+
assert.equal(existsSync(row.path), true);
|
|
2642
|
+
assert.equal(readFileSync(admissionPath, 'utf8'), admissionBefore);
|
|
2643
|
+
}
|
|
2644
|
+
finally {
|
|
2645
|
+
rmSync(root, { recursive: true, force: true });
|
|
2646
|
+
}
|
|
2647
|
+
});
|
|
2648
|
+
test.each([
|
|
2649
|
+
'direct-develop',
|
|
2650
|
+
'direct-start',
|
|
2651
|
+
'develop',
|
|
2652
|
+
'existing-start',
|
|
2653
|
+
'new-start',
|
|
2654
|
+
'scratch',
|
|
2655
|
+
'standalone',
|
|
2656
|
+
'config-resolve',
|
|
2657
|
+
])('development access preflight preserves the explicit personal override for %s', async (entry) => {
|
|
2658
|
+
const { root, runtimeEntryPath } = await fixture();
|
|
2659
|
+
const admissionPath = join(root, '.git', 'ai-delivery', 'runtime-admission.json');
|
|
2660
|
+
const admissionBefore = readFileSync(admissionPath, 'utf8');
|
|
2661
|
+
const factory = githubClient.createDeliveryGitHubClients;
|
|
2662
|
+
const actual = await vi.importActual('./github/client.js');
|
|
2663
|
+
const downstream = new Error('Reached the downstream issue API with the selected author');
|
|
2664
|
+
const selected = vi.spyOn(githubClient, 'createDeliveryGitHubClients').mockImplementation(async (input) => {
|
|
2665
|
+
const clients = await factory(input);
|
|
2666
|
+
if (input.role !== 'author' || input.personalAuth === undefined)
|
|
2667
|
+
return clients;
|
|
2668
|
+
const personal = await actual.createDeliveryGitHubClients({
|
|
2669
|
+
...input,
|
|
2670
|
+
env: { GH_TOKEN: 'synthetic-development-token' },
|
|
2671
|
+
});
|
|
2672
|
+
Object.assign(clients.rest.issues, {
|
|
2673
|
+
get: async () => {
|
|
2674
|
+
throw downstream;
|
|
2675
|
+
},
|
|
2676
|
+
create: async () => {
|
|
2677
|
+
throw downstream;
|
|
2678
|
+
},
|
|
2679
|
+
});
|
|
2680
|
+
Object.assign(clients.rest, { request: async () => Promise.reject(new Error('Rules unavailable')) });
|
|
2681
|
+
return {
|
|
2682
|
+
...personal,
|
|
2683
|
+
graphql: clients.graphql,
|
|
2684
|
+
rest: clients.rest,
|
|
2685
|
+
authenticatedAuthor: async () => ({ actorLogin: 'host-user', credentialIdentity: 'user:37' }),
|
|
2686
|
+
};
|
|
2687
|
+
});
|
|
2688
|
+
const execution = { repoRoot: root, runtimeEntryPath, identity: 'personal', personalAuth: true };
|
|
2689
|
+
const tracking = {
|
|
2690
|
+
title: 'Synthetic change',
|
|
2691
|
+
body: '## Outcome\nDeliver a verified change.\n\n## Scope\n- `artifact.txt`\n\n## Acceptance Criteria\n- [ ] Change is verified\n- [ ] Review is recorded\n\n## Verification\nRun `node --version`.',
|
|
2692
|
+
points: 2,
|
|
2693
|
+
develop: true,
|
|
2694
|
+
};
|
|
2695
|
+
try {
|
|
2696
|
+
if (entry === 'config-resolve') {
|
|
2697
|
+
// The prescribed CLI read must inspect this development selection without admission.
|
|
2698
|
+
rmSync(admissionPath);
|
|
2699
|
+
const originalArgv = process.argv;
|
|
2700
|
+
const originalExitCode = process.exitCode;
|
|
2701
|
+
let stdout = '';
|
|
2702
|
+
let stderr = '';
|
|
2703
|
+
let completed = () => { };
|
|
2704
|
+
const output = new Promise((resolve) => {
|
|
2705
|
+
completed = resolve;
|
|
2706
|
+
});
|
|
2707
|
+
const out = vi.spyOn(process.stdout, 'write').mockImplementation((value) => {
|
|
2708
|
+
stdout += String(value);
|
|
2709
|
+
completed();
|
|
2710
|
+
return true;
|
|
2711
|
+
});
|
|
2712
|
+
const err = vi.spyOn(process.stderr, 'write').mockImplementation((value) => {
|
|
2713
|
+
stderr += String(value);
|
|
2714
|
+
completed();
|
|
2715
|
+
return true;
|
|
2716
|
+
});
|
|
2717
|
+
try {
|
|
2718
|
+
process.argv = [
|
|
2719
|
+
process.execPath,
|
|
2720
|
+
'ai-delivery',
|
|
2721
|
+
'--repo-root',
|
|
2722
|
+
root,
|
|
2723
|
+
'--identity',
|
|
2724
|
+
'personal',
|
|
2725
|
+
'--personal-auth',
|
|
2726
|
+
'config:resolve',
|
|
2727
|
+
];
|
|
2728
|
+
await import('./cli.js');
|
|
2729
|
+
await output;
|
|
2730
|
+
assert.equal(stderr, '');
|
|
2731
|
+
const resolved = JSON.parse(stdout);
|
|
2732
|
+
assert.equal(resolved.reviewRoute.author.identity, 'personal');
|
|
2733
|
+
assert.equal(resolved.reviewRoute.author.authSource, 'personal');
|
|
2734
|
+
assert.equal(resolved.reviewRoute.author.actorLogin, 'host-user');
|
|
2735
|
+
assert.equal(resolved.reviewRoute.approvalEligibility, 'unknown');
|
|
2736
|
+
assert.equal(resolved.reviewRoute.rules.visibility, 'unknown');
|
|
2737
|
+
assert.equal(existsSync(admissionPath), false);
|
|
2738
|
+
assert.equal(existsSync(join(root, '.worktrees')), false);
|
|
2739
|
+
}
|
|
2740
|
+
finally {
|
|
2741
|
+
out.mockRestore();
|
|
2742
|
+
err.mockRestore();
|
|
2743
|
+
process.argv = originalArgv;
|
|
2744
|
+
process.exitCode = originalExitCode;
|
|
2745
|
+
}
|
|
2746
|
+
}
|
|
2747
|
+
else if (entry === 'scratch' || entry === 'standalone') {
|
|
2748
|
+
const row = (entry === 'scratch'
|
|
2749
|
+
? await executeTool('issue_start', { scratch: true, request: 'Inspect parser' }, execution)
|
|
2750
|
+
: await executeTool('issue_worktree_create', { name: 'scratch-override', branch: 'scratch/override' }, execution));
|
|
2751
|
+
if (entry === 'standalone')
|
|
2752
|
+
assert.equal(row.identity, 'personal');
|
|
2753
|
+
assert.equal(existsSync(entry === 'scratch' ? row.worktreePath : row.path), true);
|
|
2754
|
+
}
|
|
2755
|
+
else {
|
|
2756
|
+
const run = async () => {
|
|
2757
|
+
if (entry === 'direct-develop' || entry === 'direct-start') {
|
|
2758
|
+
const context = await contextFor(execution, 'develop');
|
|
2759
|
+
return entry === 'direct-develop' ? developIssue(context, 17) : startTrackedIssue(context, tracking);
|
|
2760
|
+
}
|
|
2761
|
+
if (entry === 'develop')
|
|
2762
|
+
return executeTool('issue_develop', { issueNumber: 17 }, execution);
|
|
2763
|
+
if (entry === 'existing-start')
|
|
2764
|
+
return executeTool('issue_start', { issueNumber: 17 }, execution);
|
|
2765
|
+
return executeTool('issue_start', tracking, execution);
|
|
2766
|
+
};
|
|
2767
|
+
await assert.rejects(run(), (error) => error === downstream);
|
|
2768
|
+
assert.equal(existsSync(join(root, '.worktrees')), false);
|
|
2769
|
+
}
|
|
2770
|
+
if (entry !== 'config-resolve')
|
|
2771
|
+
assert.equal(readFileSync(admissionPath, 'utf8'), admissionBefore);
|
|
2772
|
+
}
|
|
2773
|
+
finally {
|
|
2774
|
+
selected.mockRestore();
|
|
2775
|
+
rmSync(root, { recursive: true, force: true });
|
|
2776
|
+
}
|
|
2777
|
+
});
|
|
2528
2778
|
test('MCP exposes one implementation surface for native lifecycle commands', () => {
|
|
2529
2779
|
assert.deepEqual(AI_DELIVERY_MCP_TOOLS.map((tool) => tool.name), [
|
|
2780
|
+
'runtime_stage',
|
|
2781
|
+
'runtime_admit',
|
|
2530
2782
|
'issue_create',
|
|
2531
2783
|
'issue_start',
|
|
2532
2784
|
'issue_update',
|
|
@@ -2540,9 +2792,12 @@ test('MCP exposes one implementation surface for native lifecycle commands', ()
|
|
|
2540
2792
|
'issue_pr_merge',
|
|
2541
2793
|
'issue_finish',
|
|
2542
2794
|
'issue_worktree_create',
|
|
2795
|
+
'issue_worktree_transition_inspect',
|
|
2796
|
+
'issue_worktree_transition_apply',
|
|
2543
2797
|
]);
|
|
2544
2798
|
const validInputs = {
|
|
2545
2799
|
issue_create: { title: 'Synthetic tracking parent' },
|
|
2800
|
+
issue_update: { issueNumber: 17, park: true },
|
|
2546
2801
|
issue_ready_check: { issueNumber: 17 },
|
|
2547
2802
|
issue_develop: { issueNumber: 17 },
|
|
2548
2803
|
issue_verify: { issueNumber: 17 },
|
|
@@ -2550,7 +2805,17 @@ test('MCP exposes one implementation surface for native lifecycle commands', ()
|
|
|
2550
2805
|
issue_pr_review: { issueNumber: 17, prNumber: 23, artifact: '{}' },
|
|
2551
2806
|
issue_pr_merge: { issueNumber: 17, prNumber: 23, strategy: 'merge' },
|
|
2552
2807
|
issue_finish: { issueNumber: 17, prNumber: 23, strategy: 'merge' },
|
|
2808
|
+
issue_worktree_transition_inspect: { issueNumber: 17, purpose: 'active-resume' },
|
|
2809
|
+
issue_worktree_transition_apply: {
|
|
2810
|
+
authority: 'worktree:transition',
|
|
2811
|
+
planPath: '/synthetic/plan.json',
|
|
2812
|
+
expectedPlanId: `sha256:${'a'.repeat(64)}`,
|
|
2813
|
+
relinquishmentCommentId: 101,
|
|
2814
|
+
acceptanceCommentId: 102,
|
|
2815
|
+
},
|
|
2553
2816
|
};
|
|
2817
|
+
const updateTool = AI_DELIVERY_MCP_TOOLS.find((tool) => tool.name === 'issue_update');
|
|
2818
|
+
assert.equal(updateTool.inputSchema.safeParse({ issueNumber: 17, park: false }).success, false);
|
|
2554
2819
|
for (const [name, input] of Object.entries(validInputs)) {
|
|
2555
2820
|
const tool = AI_DELIVERY_MCP_TOOLS.find((candidate) => candidate.name === name);
|
|
2556
2821
|
assert.ok(tool, `${name} has an MCP definition`);
|
|
@@ -2585,7 +2850,11 @@ test('MCP request rejects unsupported legacy options before lifecycle dispatch',
|
|
|
2585
2850
|
}
|
|
2586
2851
|
});
|
|
2587
2852
|
async function syntheticLifecycle(routing) {
|
|
2588
|
-
const {
|
|
2853
|
+
const created = await fixture({ ...routing, omitRuntimeAdmission: routing.producerOnboarding === true });
|
|
2854
|
+
const root = created.root;
|
|
2855
|
+
let runtimeEntryPath = created.runtimeEntryPath;
|
|
2856
|
+
const originalPath = process.env.PATH;
|
|
2857
|
+
let restoreTransport;
|
|
2589
2858
|
const remoteName = routing.remote;
|
|
2590
2859
|
assert.equal(defaultBaseRef(root, remoteName), `refs/remotes/${remoteName}/main`);
|
|
2591
2860
|
const remote = join(root, '.git', 'remote.git');
|
|
@@ -2614,6 +2883,9 @@ async function syntheticLifecycle(routing) {
|
|
|
2614
2883
|
let trackingParent = false;
|
|
2615
2884
|
let failFirstBlockerLink = true;
|
|
2616
2885
|
let failNextProjectSync = false;
|
|
2886
|
+
let interruptNextStatusUpdate = false;
|
|
2887
|
+
let conflictNextStatusReadback = false;
|
|
2888
|
+
let projectStatusWrites = 0;
|
|
2617
2889
|
let checksPassed = true;
|
|
2618
2890
|
let prDraft = true;
|
|
2619
2891
|
let prState = 'open';
|
|
@@ -2720,6 +2992,12 @@ async function syntheticLifecycle(routing) {
|
|
|
2720
2992
|
nodes: projectStatus === null
|
|
2721
2993
|
? []
|
|
2722
2994
|
: [
|
|
2995
|
+
{
|
|
2996
|
+
id: 'UNRELATED-ITEM',
|
|
2997
|
+
isArchived: false,
|
|
2998
|
+
content: { id: 'UNRELATED-ISSUE' },
|
|
2999
|
+
fieldValueByName: { name: 'Active', optionId: 'STATUS-1' },
|
|
3000
|
+
},
|
|
2723
3001
|
{
|
|
2724
3002
|
id: 'ITEM-17',
|
|
2725
3003
|
isArchived: false,
|
|
@@ -2743,19 +3021,29 @@ async function syntheticLifecycle(routing) {
|
|
|
2743
3021
|
return { addProjectV2ItemById: { item: { id: 'ITEM-17' } } };
|
|
2744
3022
|
}
|
|
2745
3023
|
if (query.includes('UpdateProjectDeliveryStatus')) {
|
|
3024
|
+
assert.equal(variables.projectId, 'PROJECT-1');
|
|
3025
|
+
assert.equal(variables.itemId, 'ITEM-17');
|
|
3026
|
+
projectStatusWrites += 1;
|
|
2746
3027
|
projectStatus = statusName(String(variables.optionId)) ?? null;
|
|
3028
|
+
if (interruptNextStatusUpdate) {
|
|
3029
|
+
interruptNextStatusUpdate = false;
|
|
3030
|
+
throw new Error('synthetic ambiguous status write');
|
|
3031
|
+
}
|
|
2747
3032
|
return { updateProjectV2ItemFieldValue: { projectV2Item: { id: 'ITEM-17' } } };
|
|
2748
3033
|
}
|
|
2749
|
-
if (query.includes('ProjectDeliveryItemReadback'))
|
|
3034
|
+
if (query.includes('ProjectDeliveryItemReadback')) {
|
|
3035
|
+
const contentId = conflictNextStatusReadback ? 'UNRELATED-ISSUE' : issue.node_id;
|
|
3036
|
+
conflictNextStatusReadback = false;
|
|
2750
3037
|
return {
|
|
2751
3038
|
node: {
|
|
2752
3039
|
id: 'ITEM-17',
|
|
2753
3040
|
isArchived: false,
|
|
2754
3041
|
project: { id: 'PROJECT-1', number: 1 },
|
|
2755
|
-
content: { id:
|
|
3042
|
+
content: { id: contentId },
|
|
2756
3043
|
fieldValueByName: { name: projectStatus, optionId: statuses[projectStatus] },
|
|
2757
3044
|
},
|
|
2758
3045
|
};
|
|
3046
|
+
}
|
|
2759
3047
|
if (query.includes('blockedBy(first:'))
|
|
2760
3048
|
return {
|
|
2761
3049
|
repository: {
|
|
@@ -2776,6 +3064,10 @@ async function syntheticLifecycle(routing) {
|
|
|
2776
3064
|
blockerOpen = true;
|
|
2777
3065
|
return { addBlockedBy: { issue: { number: issueNumber } } };
|
|
2778
3066
|
}
|
|
3067
|
+
if (query.includes('removeBlockedBy')) {
|
|
3068
|
+
blockerOpen = false;
|
|
3069
|
+
return { removeBlockedBy: { issue: { number: issueNumber } } };
|
|
3070
|
+
}
|
|
2779
3071
|
if (query.includes('RepositoryId'))
|
|
2780
3072
|
return { repository: { id: 'REPO-1' } };
|
|
2781
3073
|
if (query.includes('UpdateExactRefs')) {
|
|
@@ -2948,6 +3240,65 @@ async function syntheticLifecycle(routing) {
|
|
|
2948
3240
|
};
|
|
2949
3241
|
let restoreDispatchClient;
|
|
2950
3242
|
try {
|
|
3243
|
+
if (routing.producerOnboarding) {
|
|
3244
|
+
assert.equal(existsSync(join(root, 'AGENTS.md')), false);
|
|
3245
|
+
const archiveBase = join(root, '.git', 'producer-fixture');
|
|
3246
|
+
const packageSource = join(archiveBase, 'package');
|
|
3247
|
+
mkdirSync(archiveBase);
|
|
3248
|
+
cpSync(dirname(dirname(created.runtimeEntryPath)), packageSource, { recursive: true });
|
|
3249
|
+
cpSync(join(root, '.git/synthetic-runtime/plugin'), join(packageSource, 'plugins/ai-delivery'), {
|
|
3250
|
+
recursive: true,
|
|
3251
|
+
});
|
|
3252
|
+
const archive = join(archiveBase, 'reviewed.tgz');
|
|
3253
|
+
execFileSync('tar', ['-czf', archive, '-C', archiveBase, 'package']);
|
|
3254
|
+
const shimDir = join(archiveBase, 'bin');
|
|
3255
|
+
mkdirSync(shimDir);
|
|
3256
|
+
const npmShim = join(shimDir, 'npm');
|
|
3257
|
+
// Only installer and GitHub transports are synthetic; the public producer and validator are real.
|
|
3258
|
+
writeFileSync(npmShim, `#!${process.execPath}\nconst fs=require('fs'),path=require('path');const args=process.argv.slice(2);if(args[0]!=='install'||!['--omit=dev','--ignore-scripts','--no-audit','--no-fund'].every(x=>args.includes(x)))process.exit(91);const target=path.join(args[args.indexOf('--prefix')+1],'node_modules/@aviaratech/ai-delivery');fs.mkdirSync(path.dirname(target),{recursive:true});fs.cpSync(${JSON.stringify(packageSource)},target,{recursive:true});\n`);
|
|
3259
|
+
chmodSync(npmShim, 0o700);
|
|
3260
|
+
process.env.PATH = `${shimDir}:${originalPath ?? ''}`;
|
|
3261
|
+
const server = createAiDeliveryMcpServer({ repoRoot: root, identity: 'synthetic-author' });
|
|
3262
|
+
const client = new Client({ name: 'unrelated-public-onboarding', version: '1.0.0' });
|
|
3263
|
+
const [ct, st] = InMemoryTransport.createLinkedPair();
|
|
3264
|
+
await server.connect(st);
|
|
3265
|
+
await client.connect(ct);
|
|
3266
|
+
const setup = {
|
|
3267
|
+
expectedSourceCommit: baseSha,
|
|
3268
|
+
expectedConfigDigest: configuration.configDigest,
|
|
3269
|
+
runtimeDirectory: join(root, '.git', 'public-runtime-stage'),
|
|
3270
|
+
};
|
|
3271
|
+
try {
|
|
3272
|
+
const stageResponse = await client.callTool({
|
|
3273
|
+
name: 'runtime_stage',
|
|
3274
|
+
arguments: {
|
|
3275
|
+
...setup,
|
|
3276
|
+
authority: 'runtime:stage',
|
|
3277
|
+
archivePath: archive,
|
|
3278
|
+
expectedArchiveSha256: sha256(archive),
|
|
3279
|
+
packageVersion: '0.1.0',
|
|
3280
|
+
},
|
|
3281
|
+
});
|
|
3282
|
+
assert.equal(stageResponse.isError, undefined);
|
|
3283
|
+
const stage = JSON.parse(stageResponse.content[0].text);
|
|
3284
|
+
const admitted = await client.callTool({
|
|
3285
|
+
name: 'runtime_admit',
|
|
3286
|
+
arguments: {
|
|
3287
|
+
...setup,
|
|
3288
|
+
authority: 'runtime:admit',
|
|
3289
|
+
stageId: stage.stageId,
|
|
3290
|
+
expectedPriorAdmissionSha256: null,
|
|
3291
|
+
},
|
|
3292
|
+
});
|
|
3293
|
+
assert.equal(admitted.isError, undefined);
|
|
3294
|
+
runtimeEntryPath = stage.admission.cliPath;
|
|
3295
|
+
}
|
|
3296
|
+
finally {
|
|
3297
|
+
await client.close();
|
|
3298
|
+
await server.close();
|
|
3299
|
+
}
|
|
3300
|
+
await assertDeliveryRuntimeAdmitted({ repoRoot: root, runtimeEntryPath });
|
|
3301
|
+
}
|
|
2951
3302
|
await assert.rejects(startTrackedIssue(context, { request: 'Unready task', develop: true }), /New issue is not ready/u);
|
|
2952
3303
|
assert.equal(calls.filter((call) => call === 'issue:create').length, 0);
|
|
2953
3304
|
const unsized = await createIssue(context, { title: 'Synthetic child', parentIssueNumber: 11 });
|
|
@@ -3013,36 +3364,107 @@ async function syntheticLifecycle(routing) {
|
|
|
3013
3364
|
assert.equal(started.title, issue.title);
|
|
3014
3365
|
const row = started;
|
|
3015
3366
|
assert.equal(projectStatus, 'Active');
|
|
3367
|
+
const parked = await updateIssue(context, { issueNumber, park: true });
|
|
3368
|
+
assert.equal(parked.projectStatus, 'Todo');
|
|
3369
|
+
assert.equal(existsSync(row.path), true);
|
|
3370
|
+
assert.equal((await updateIssue(context, { issueNumber, title: issue.title })).projectStatus, 'Todo');
|
|
3371
|
+
await developIssue(context, issueNumber);
|
|
3372
|
+
assert.equal(projectStatus, 'Active');
|
|
3373
|
+
const blockedUpdate = await updateIssue(context, { issueNumber, blockedBy: [9] });
|
|
3374
|
+
assert.deepEqual(blockedUpdate.blockedBy, [9]);
|
|
3375
|
+
assert.equal(blockedUpdate.projectStatus, 'Blocked');
|
|
3376
|
+
assert.equal(projectStatus, 'Waiting');
|
|
3377
|
+
const blockedWrites = projectStatusWrites;
|
|
3378
|
+
assert.equal((await updateIssue(context, { issueNumber, blockedBy: [9] })).projectStatus, 'Blocked');
|
|
3379
|
+
assert.equal(projectStatusWrites, blockedWrites);
|
|
3380
|
+
assert.equal((await updateIssue(context, { issueNumber, park: true })).projectStatus, 'Blocked');
|
|
3381
|
+
await assert.rejects(developIssue(context, issueNumber), /not ready/u);
|
|
3382
|
+
interruptNextStatusUpdate = true;
|
|
3383
|
+
await assert.rejects(updateIssue(context, { issueNumber, blockedBy: [] }), /ambiguous status write/u);
|
|
3384
|
+
assert.equal(blockerOpen, false);
|
|
3385
|
+
assert.equal(projectStatus, 'Queued');
|
|
3386
|
+
const interruptedWrites = projectStatusWrites;
|
|
3387
|
+
const unblockedUpdate = await updateIssue(context, { issueNumber, blockedBy: [] });
|
|
3388
|
+
assert.equal(projectStatusWrites, interruptedWrites);
|
|
3389
|
+
assert.deepEqual(unblockedUpdate.blockedBy, []);
|
|
3390
|
+
assert.equal(unblockedUpdate.projectStatus, 'Todo');
|
|
3391
|
+
conflictNextStatusReadback = true;
|
|
3392
|
+
await assert.rejects(updateIssue(context, { issueNumber, blockedBy: [9] }), /exact issue item/u);
|
|
3393
|
+
assert.equal(blockerOpen, true);
|
|
3394
|
+
assert.equal((await updateIssue(context, { issueNumber, blockedBy: [9] })).projectStatus, 'Blocked');
|
|
3395
|
+
assert.equal((await updateIssue(context, { issueNumber, state: 'closed' })).projectStatus, 'Done');
|
|
3396
|
+
assert.equal((await updateIssue(context, { issueNumber, park: true })).projectStatus, 'Done');
|
|
3397
|
+
assert.equal((await resumeCreatedIssue(context, { issueNumber })).projectStatus, 'Done');
|
|
3398
|
+
assert.equal((await updateIssue(context, { issueNumber, state: 'open', blockedBy: [] })).projectStatus, 'Done');
|
|
3399
|
+
assert.equal(existsSync(row.path), true);
|
|
3400
|
+
await developIssue(context, issueNumber);
|
|
3401
|
+
assert.equal(projectStatus, 'Active');
|
|
3016
3402
|
writeFileSync(join(row.path, 'change.txt'), 'feature\n');
|
|
3017
3403
|
git(row.path, 'add', 'change.txt');
|
|
3018
3404
|
git(row.path, 'commit', '-qm', 'synthetic change');
|
|
3019
3405
|
headSha = git(row.path, 'rev-parse', 'HEAD');
|
|
3020
3406
|
const run = await verifyIssue({ issueNumber, repoRoot: row.path });
|
|
3021
3407
|
await assert.rejects(publishPr(context, { issueNumber, body: 'Closes #17' }), /Delivery Impact/);
|
|
3022
|
-
|
|
3023
|
-
|
|
3024
|
-
|
|
3025
|
-
|
|
3026
|
-
|
|
3027
|
-
|
|
3028
|
-
|
|
3029
|
-
|
|
3030
|
-
|
|
3031
|
-
|
|
3032
|
-
|
|
3033
|
-
|
|
3034
|
-
|
|
3035
|
-
|
|
3036
|
-
|
|
3037
|
-
|
|
3038
|
-
|
|
3039
|
-
|
|
3040
|
-
|
|
3041
|
-
|
|
3042
|
-
|
|
3043
|
-
|
|
3044
|
-
|
|
3045
|
-
|
|
3408
|
+
if (routing.producerOnboarding) {
|
|
3409
|
+
const realGit = execFileSync('/usr/bin/which', ['git'], {
|
|
3410
|
+
encoding: 'utf8',
|
|
3411
|
+
env: { ...process.env, PATH: originalPath },
|
|
3412
|
+
}).trim();
|
|
3413
|
+
const shimDir = join(root, '.git/producer-fixture/bin');
|
|
3414
|
+
git(root, 'config', '--unset', `url.${remote}.insteadOf`);
|
|
3415
|
+
const gitShim = join(shimDir, 'git');
|
|
3416
|
+
writeFileSync(gitShim, `#!/bin/sh
|
|
3417
|
+
if [ "$1" = -c ] && [ "$2" = credential.helper= ]; then
|
|
3418
|
+
[ "$AI_DELIVERY_GIT_TOKEN" = synthetic-selected-author-token ] || exit 92
|
|
3419
|
+
shift 2
|
|
3420
|
+
operation="$1"; shift
|
|
3421
|
+
[ "$1" = https://github.com/example/widget.git ] || exit 93
|
|
3422
|
+
shift
|
|
3423
|
+
exec "${realGit}" -c credential.helper= "$operation" "${remote}" "$@"
|
|
3424
|
+
fi
|
|
3425
|
+
if [ "$1" = push ] && [ "$2" = -q ] && [ "$3" = "${remoteName}" ]; then
|
|
3426
|
+
shift 3
|
|
3427
|
+
exec "${realGit}" push -q "${remote}" "$@"
|
|
3428
|
+
fi
|
|
3429
|
+
exec "${realGit}" "$@"
|
|
3430
|
+
`);
|
|
3431
|
+
chmodSync(gitShim, 0o700);
|
|
3432
|
+
const transport = vi.spyOn(githubClient, 'withAuthorGitToken').mockImplementation(async (clients, callback) => {
|
|
3433
|
+
assert.equal(clients.role, 'author');
|
|
3434
|
+
return callback('synthetic-selected-author-token');
|
|
3435
|
+
});
|
|
3436
|
+
restoreTransport = () => transport.mockRestore();
|
|
3437
|
+
const published = await publishPr(context, { issueNumber });
|
|
3438
|
+
assert.equal(published.prNumber, prNumber);
|
|
3439
|
+
assert.equal(published.reviewRoute?.author.actorLogin, 'synthetic-author[bot]');
|
|
3440
|
+
assert.equal(published.reviewRoute?.approvalEligibility, 'unknown');
|
|
3441
|
+
}
|
|
3442
|
+
else {
|
|
3443
|
+
await assert.rejects(publishPr(context, { issueNumber }), /Git push requires the selected author credential token/);
|
|
3444
|
+
// The public push requires a real author App installation token. Bind a synthetic
|
|
3445
|
+
// publication at that external boundary so the remaining public phases run offline.
|
|
3446
|
+
git(row.path, 'push', '-q', remoteName, `${headSha}:refs/heads/issue/17`);
|
|
3447
|
+
const publishEvidence = await createIssuePhaseEvidence({ issueNumber, phase: 'publish', repoRoot: row.path });
|
|
3448
|
+
const publicationContent = {
|
|
3449
|
+
baseSha,
|
|
3450
|
+
evidenceId: publishEvidence.evidenceId,
|
|
3451
|
+
headSha,
|
|
3452
|
+
issueNumber,
|
|
3453
|
+
prNumber,
|
|
3454
|
+
schemaVersion: 'ai-delivery.publication@1',
|
|
3455
|
+
};
|
|
3456
|
+
const publicationPath = join(gitCommonDir(row.path), 'ai-delivery', 'publications', String(issueNumber), `${headSha}.json`);
|
|
3457
|
+
mkdirSync(dirname(publicationPath), { recursive: true });
|
|
3458
|
+
writeFileSync(publicationPath, JSON.stringify({ ...publicationContent, publicationId: digestValue(publicationContent) }), { mode: 0o600 });
|
|
3459
|
+
await updateIssueWorktreeDelivery({
|
|
3460
|
+
branch: row.branch,
|
|
3461
|
+
issueNumber,
|
|
3462
|
+
path: row.path,
|
|
3463
|
+
prNumber,
|
|
3464
|
+
projectRoot: root,
|
|
3465
|
+
status: 'pr-published',
|
|
3466
|
+
});
|
|
3467
|
+
}
|
|
3046
3468
|
assert.equal((await listPrs(context)).pullRequests[0]?.number, prNumber);
|
|
3047
3469
|
assert.equal((await prInfo(context, { prNumber })).authorLogin, 'synthetic-author[bot]');
|
|
3048
3470
|
assert.equal((await prChecks(context, prNumber)).combinedStatus, 'success');
|
|
@@ -3188,6 +3610,8 @@ async function syntheticLifecycle(routing) {
|
|
|
3188
3610
|
assert.equal(calls.filter((call) => call === 'issue:create').length, beforeTracking + 1);
|
|
3189
3611
|
}
|
|
3190
3612
|
finally {
|
|
3613
|
+
restoreTransport?.();
|
|
3614
|
+
process.env.PATH = originalPath;
|
|
3191
3615
|
restoreDispatchClient?.();
|
|
3192
3616
|
rmSync(root, { recursive: true, force: true });
|
|
3193
3617
|
}
|
|
@@ -3197,4 +3621,46 @@ test.each([
|
|
|
3197
3621
|
{ remote: 'upstream', divergentOrigin: false },
|
|
3198
3622
|
{ remote: 'upstream', divergentOrigin: true },
|
|
3199
3623
|
])('synthetic lifecycle finishes with remote $remote and divergent origin $divergentOrigin', syntheticLifecycle, 15_000);
|
|
3624
|
+
test('an unrelated consumer uses public MCP setup through verified configured-author publication, counted review readback, merge and cleanup', { timeout: 0 }, async () => {
|
|
3625
|
+
await syntheticLifecycle({ remote: 'origin', divergentOrigin: false, producerOnboarding: true });
|
|
3626
|
+
});
|
|
3627
|
+
test.each(['escape', 'broken', 'cycle'])('output observation rejects %s aliases without running policy commands', async (kind) => {
|
|
3628
|
+
const { root, counter } = await fixture();
|
|
3629
|
+
const base = realpathSync(mkdtempSync(join(tmpdir(), 'ai-delivery-output-alias-')));
|
|
3630
|
+
try {
|
|
3631
|
+
const output = join(base, 'output');
|
|
3632
|
+
mkdirSync(output);
|
|
3633
|
+
if (kind === 'escape') {
|
|
3634
|
+
writeFileSync(join(base, 'outside'), 'outside');
|
|
3635
|
+
symlinkSync('../outside', join(output, 'alias'));
|
|
3636
|
+
}
|
|
3637
|
+
if (kind === 'broken')
|
|
3638
|
+
symlinkSync('./absent', join(output, 'alias'));
|
|
3639
|
+
if (kind === 'cycle') {
|
|
3640
|
+
symlinkSync('./other', join(output, 'alias'));
|
|
3641
|
+
symlinkSync('./alias', join(output, 'other'));
|
|
3642
|
+
}
|
|
3643
|
+
const row = await prepareIssueWorktree({
|
|
3644
|
+
baseRef: 'main',
|
|
3645
|
+
identity: 'synthetic-author',
|
|
3646
|
+
issueNumber: 17,
|
|
3647
|
+
repoRoot: root,
|
|
3648
|
+
});
|
|
3649
|
+
await assert.rejects(verifyIssue({
|
|
3650
|
+
issueNumber: 17,
|
|
3651
|
+
repoRoot: row.path,
|
|
3652
|
+
resourceBounds: {
|
|
3653
|
+
maxAggregateRssBytes: 1_000_000_000,
|
|
3654
|
+
minFreeDiskBytes: 1,
|
|
3655
|
+
maxNewOutputBytes: 1024,
|
|
3656
|
+
outputRoots: [output],
|
|
3657
|
+
},
|
|
3658
|
+
}), /escaping|broken|cyclic/u);
|
|
3659
|
+
assert.equal(existsSync(counter), false);
|
|
3660
|
+
}
|
|
3661
|
+
finally {
|
|
3662
|
+
rmSync(root, { recursive: true, force: true });
|
|
3663
|
+
rmSync(base, { recursive: true, force: true });
|
|
3664
|
+
}
|
|
3665
|
+
});
|
|
3200
3666
|
//# sourceMappingURL=lifecycle.test.js.map
|