Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -4,6 +4,8 @@ import { dirname, join, resolve } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import YAML from 'yaml';
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest';
|
||||
import { loadFleetRoster } from '../commands/fleet.js';
|
||||
import { executeFleetReconcile } from './fleet-reconciler.js';
|
||||
import { compareCodePoints } from './deterministic-order.js';
|
||||
import { SHIPPED_FLEET_ARTIFACT_DISPOSITIONS } from './example-profile-dispositions.js';
|
||||
import {
|
||||
@@ -11,6 +13,7 @@ import {
|
||||
previewV1ToV2Migration,
|
||||
validateShippedFleetMigrationDispositions,
|
||||
type V1ToV2MigrationDecisions,
|
||||
type V1ToV2MigrationPreview,
|
||||
} from './v1-v2-migration.js';
|
||||
|
||||
const temporaryDirectories: string[] = [];
|
||||
@@ -529,6 +532,122 @@ describe('v1-to-v2 migration preview', (): void => {
|
||||
});
|
||||
});
|
||||
|
||||
it('matches production v1 reset defaults when a declared runtime config is empty', async () => {
|
||||
const source = v1Source.replace(' pi:\n reset_command: /new', ' pi: {}');
|
||||
const productionSource = source
|
||||
.replace(/ - name: remote0[\s\S]*?connector:/, 'connector:')
|
||||
.replace(/connector:[\s\S]*$/, '');
|
||||
const rosterPath = join(await mkdtemp(join(tmpdir(), 'v1-runtime-parity-')), 'roster.yaml');
|
||||
temporaryDirectories.push(dirname(rosterPath));
|
||||
await writeFile(rosterPath, productionSource);
|
||||
const productionRoster = await loadFleetRoster(rosterPath);
|
||||
const preview = await previewV1ToV2Migration({
|
||||
source,
|
||||
decisions: decisions(),
|
||||
observations: observations(),
|
||||
personaDirs: await semanticDirs(),
|
||||
});
|
||||
|
||||
expect(preview.status).toBe('ready');
|
||||
expect(productionRoster.runtimes.pi?.resetCommand).toBe('/clear');
|
||||
expect(preview.canonicalRoster?.runtimes.pi?.resetCommand).toBe(
|
||||
productionRoster.runtimes.pi?.resetCommand,
|
||||
);
|
||||
});
|
||||
|
||||
it('emits byte-identical preview evidence when equivalent local agents are reordered', async () => {
|
||||
const coder1 = ` - name: coder1
|
||||
alias: Coder One
|
||||
provider: openai
|
||||
runtime: pi
|
||||
class: reviewer
|
||||
working_directory: /srv/coder1
|
||||
reasoning_level: high
|
||||
tool_policy: reviewer
|
||||
`;
|
||||
const sourceA = v1Source.replace(' - name: remote0', `${coder1} - name: remote0`);
|
||||
const sourceB = sourceA.replace(
|
||||
/( - name: coder0[\s\S]*?)( - name: coder1[\s\S]*?)( - name: remote0)/,
|
||||
'$2$1$3',
|
||||
);
|
||||
const migrationDecisions = decisions();
|
||||
migrationDecisions.agents.coder1 = {
|
||||
provider: 'openai',
|
||||
model: 'gpt-5.6-sol',
|
||||
reasoning: 'high',
|
||||
enabled: true,
|
||||
launchYolo: false,
|
||||
};
|
||||
const lifecycle = {
|
||||
...observations(),
|
||||
coder1: { systemd: 'active' as const, tmux: 'present' as const },
|
||||
};
|
||||
const dirs = await semanticDirs();
|
||||
const previews = await Promise.all(
|
||||
[sourceA, sourceB].map((source) =>
|
||||
previewV1ToV2Migration({
|
||||
source,
|
||||
decisions: migrationDecisions,
|
||||
observations: lifecycle,
|
||||
personaDirs: dirs,
|
||||
}),
|
||||
),
|
||||
);
|
||||
const previewA = previews[0]!;
|
||||
const previewB = previews[1]!;
|
||||
|
||||
expect(previewA.status).toBe('ready');
|
||||
expect(previewB.status).toBe('ready');
|
||||
expect(previewB.evidence.observations).toEqual(previewA.evidence.observations);
|
||||
expect({
|
||||
...previewB.evidence,
|
||||
source: { ...previewB.evidence.source, sha256: '<source-specific>' },
|
||||
}).toEqual({
|
||||
...previewA.evidence,
|
||||
source: { ...previewA.evidence.source, sha256: '<source-specific>' },
|
||||
});
|
||||
expect(previewB.canonicalYaml).toBe(previewA.canonicalYaml);
|
||||
});
|
||||
|
||||
it('emits byte-identical preview evidence when equivalent remote agents are reordered', async () => {
|
||||
const remote1 = ` - name: remote1
|
||||
runtime: pi
|
||||
class: reviewer
|
||||
host: review.example.test
|
||||
ssh: operator@review.example.test
|
||||
socket: review-fleet
|
||||
`;
|
||||
const sourceA = v1Source.replace('connector:', `${remote1}connector:`);
|
||||
const sourceB = sourceA.replace(
|
||||
/( - name: remote0[\s\S]*?)( - name: remote1[\s\S]*?)(connector:)/,
|
||||
'$2$1$3',
|
||||
);
|
||||
const dirs = await semanticDirs();
|
||||
const previews = await Promise.all(
|
||||
[sourceA, sourceB].map((source) =>
|
||||
previewV1ToV2Migration({
|
||||
source,
|
||||
decisions: decisions(),
|
||||
observations: observations(),
|
||||
personaDirs: dirs,
|
||||
}),
|
||||
),
|
||||
);
|
||||
const previewA = previews[0]!;
|
||||
const previewB = previews[1]!;
|
||||
const semanticOutput = (preview: V1ToV2MigrationPreview): object => ({
|
||||
...preview,
|
||||
evidence: {
|
||||
...preview.evidence,
|
||||
source: { ...preview.evidence.source, sha256: '<source-specific>' },
|
||||
},
|
||||
});
|
||||
|
||||
expect(previewA.status).toBe('ready');
|
||||
expect(previewB.status).toBe('ready');
|
||||
expect(semanticOutput(previewB)).toEqual(semanticOutput(previewA));
|
||||
});
|
||||
|
||||
it('emits byte-identical canonical evidence regardless of locale collation', async () => {
|
||||
expect(['😀', 'z', '', 'ä'].sort(compareCodePoints)).toEqual(['z', 'ä', '', '😀']);
|
||||
const dirs = await semanticDirs();
|
||||
@@ -797,6 +916,48 @@ describe('v1-to-v2 migration preview', (): void => {
|
||||
).toContain(forbiddenValue);
|
||||
});
|
||||
|
||||
it('feeds a ready home-relative candidate through the production projection boundary', async () => {
|
||||
const source = v1Source.replace('working_directory: /srv/coder0', 'working_directory: ~/src');
|
||||
const preview = await previewV1ToV2Migration({
|
||||
source,
|
||||
decisions: decisions(),
|
||||
observations: observations(),
|
||||
personaDirs: await semanticDirs(),
|
||||
});
|
||||
expect(preview.status).toBe('ready');
|
||||
expect(preview.canonicalRoster?.agents[0]?.workingDirectory).toBe('~/src');
|
||||
expect(preview.canonicalYaml).toContain('working_directory: ~/src');
|
||||
|
||||
const canonicalRoster = preview.canonicalRoster!;
|
||||
const mosaicHome = await mkdtemp(join(tmpdir(), 'v1-v2-production-projection-'));
|
||||
temporaryDirectories.push(mosaicHome);
|
||||
const fleetDir = join(mosaicHome, 'fleet');
|
||||
const agentEnvDir = join(fleetDir, 'agents');
|
||||
await mkdir(agentEnvDir, { recursive: true, mode: 0o700 });
|
||||
await chmod(mosaicHome, 0o700);
|
||||
await chmod(fleetDir, 0o700);
|
||||
await chmod(agentEnvDir, 0o700);
|
||||
|
||||
const projectionDirs = await semanticDirs();
|
||||
await expect(
|
||||
executeFleetReconcile({
|
||||
roster: canonicalRoster,
|
||||
command: 'plan',
|
||||
deps: {
|
||||
mosaicHome,
|
||||
rolesDir: projectionDirs.rolesDir,
|
||||
overrideDir: projectionDirs.overrideDir,
|
||||
runner: async (command, args) => {
|
||||
if (command === 'tmux' && args.includes('list-sessions')) {
|
||||
return { stdout: '', stderr: '', exitCode: 1 };
|
||||
}
|
||||
return { stdout: 'ActiveState=inactive\n', stderr: '', exitCode: 0 };
|
||||
},
|
||||
},
|
||||
}),
|
||||
).resolves.toMatchObject({ applied: false, projections: 'not-applied' });
|
||||
});
|
||||
|
||||
it('blocks undeclared v1 fields, synonym collisions, and duplicate names', async () => {
|
||||
const baseOptions = {
|
||||
decisions: decisions(),
|
||||
|
||||
Reference in New Issue
Block a user