mirror of
https://github.com/PR0M3TH3AN/TORCH.git
synced 2026-10-27 16:01:54 +00:00
379 lines
21 KiB
JavaScript
379 lines
21 KiB
JavaScript
import assert from 'node:assert/strict';
|
|
import { execFileSync, spawnSync } from 'node:child_process';
|
|
import { existsSync, mkdtempSync, readFileSync, writeFileSync } from 'node:fs';
|
|
import { tmpdir } from 'node:os';
|
|
import { join } from 'node:path';
|
|
import test from 'node:test';
|
|
import { openControlPlane } from '../../src/control-plane/service.mjs';
|
|
import { FleetEvolutionService } from '../../src/evolution/service.mjs';
|
|
import { analyzeRepository } from '../../src/kernel/analyze.mjs';
|
|
import { proposeDomains } from '../../src/kernel/domains.mjs';
|
|
import { inspectRepository } from '../../src/kernel/git.mjs';
|
|
import { installProject } from '../../src/kernel/install.mjs';
|
|
import { callTorchTool } from '../../src/mcp/tools.mjs';
|
|
import { createCodexAdapter } from '../../src/adapters/codex.mjs';
|
|
import { planAreaUp, startFleet } from '../../src/runtime/lifecycle.mjs';
|
|
import { observeProject } from '../../src/observability/snapshot.mjs';
|
|
|
|
const CLI = new URL('../../bin/torch.mjs', import.meta.url).pathname;
|
|
|
|
function fixture({ boundary = false } = {}) {
|
|
const root = mkdtempSync(join(tmpdir(), 'torch-evolution-'));
|
|
const worktreeParent = mkdtempSync(join(tmpdir(), 'torch-evolution-worktrees-'));
|
|
const env = { ...process.env, XDG_DATA_HOME: mkdtempSync(join(tmpdir(), 'torch-evolution-state-')) };
|
|
execFileSync('git', ['init', '-b', 'main', root]);
|
|
execFileSync('git', ['-C', root, 'config', 'user.email', '[email protected]']);
|
|
execFileSync('git', ['-C', root, 'config', 'user.name', 'TORCH Test']);
|
|
writeFileSync(join(root, 'app.js'), 'export const app = true;\n');
|
|
execFileSync('git', ['-C', root, 'add', '.']);
|
|
execFileSync('git', ['-C', root, 'commit', '-m', 'fixture']);
|
|
let repository = inspectRepository(root);
|
|
const proposal = proposeDomains({ repository, analysis: analyzeRepository(repository) });
|
|
if (boundary) {
|
|
const template = proposal.domains[0];
|
|
proposal.domains = [
|
|
{
|
|
...template, id: 'alpha', title: 'Alpha', owned_paths: ['app.js', 'src/alpha/**'],
|
|
neighbours: ['beta'], evidence: ['app.js'],
|
|
},
|
|
{
|
|
...template, id: 'beta', title: 'Beta', owned_paths: ['src/beta/**'],
|
|
neighbours: ['alpha'], evidence: ['app.js'],
|
|
},
|
|
];
|
|
}
|
|
proposal.paths = { worktree_parent: worktreeParent };
|
|
proposal.review = {
|
|
status: 'approved', reviewedAt: '2026-09-27T00:00:00Z', reviewedBy: 'fixture-owner', notes: [],
|
|
};
|
|
installProject({ repository, proposal, env, projectId: 'evolution-fixture' });
|
|
execFileSync('git', ['-C', root, 'add', '.torch']);
|
|
execFileSync('git', ['-C', root, 'commit', '-m', 'install torch']);
|
|
repository = inspectRepository(root);
|
|
return { root, env, repository, worktreeParent };
|
|
}
|
|
|
|
function boundaryBenefit() {
|
|
return {
|
|
summary: 'Align persistent sessions with the recurring implementation boundary.',
|
|
recurringWork: 'The same cross-domain changes recur across milestones.',
|
|
contextLocality: 'Keep only the context needed by each durable responsibility.',
|
|
coordinationCost: 'Reduce recurring handoffs without hiding the migration cost.',
|
|
};
|
|
}
|
|
|
|
test('SCN-fleet-boundary-evolution: merge and split proposals are durable, owner-gated, and non-mutating', () => {
|
|
const context = fixture({ boundary: true });
|
|
const configPath = join(context.root, '.torch', 'torch.yaml');
|
|
const rosterPath = join(context.root, '.torch', 'roster.yaml');
|
|
const before = { config: readFileSync(configPath, 'utf8'), roster: readFileSync(rosterPath, 'utf8') };
|
|
const control = openControlPlane({ repositoryRoot: context.root, env: context.env });
|
|
let sequence = 0;
|
|
const evolution = new FleetEvolutionService({
|
|
repositoryRoot: context.root, controlPlane: control,
|
|
clock: () => new Date('2026-09-27T14:00:00Z'), idFactory: () => `BOUNDARY-${++sequence}`,
|
|
});
|
|
const benefit = boundaryBenefit();
|
|
const mergeInput = {
|
|
sourceDomains: ['alpha', 'beta'],
|
|
resultDomains: [{
|
|
id: 'platform', title: 'Platform', kind: 'development',
|
|
scope: ['combined alpha and beta implementation'], not_scope: ['Session Manager work'],
|
|
owned_paths: ['app.js', 'src/alpha/**', 'src/beta/**'], shared_paths: [], neighbours: [],
|
|
required_checks: [], resources: [], runtime: 'claude',
|
|
}],
|
|
ownershipAssignments: [
|
|
{ sourceDomainId: 'alpha', sourcePath: 'app.js', resultDomainId: 'platform' },
|
|
{ sourceDomainId: 'alpha', sourcePath: 'src/alpha/**', resultDomainId: 'platform' },
|
|
{ sourceDomainId: 'beta', sourcePath: 'src/beta/**', resultDomainId: 'platform' },
|
|
],
|
|
rationale: 'Alpha and beta now change together and retain mostly identical context.',
|
|
expectedBenefit: benefit, evidence: ['TASK-31', 'TASK-38'],
|
|
};
|
|
assert.throws(
|
|
() => evolution.proposeMerge({ ...mergeInput, proposer: 'alpha' }),
|
|
(error) => error.code === 'FLEET_CHANGE_AUTHORITY_REQUIRED',
|
|
);
|
|
assert.throws(
|
|
() => evolution.proposeMerge({
|
|
...mergeInput, proposer: 'session-manager', ownershipAssignments: mergeInput.ownershipAssignments.slice(1),
|
|
}),
|
|
(error) => error.code === 'FLEET_OWNERSHIP_ASSIGNMENT_INVALID',
|
|
);
|
|
const merged = callTorchTool(control, 'torch_propose_domain_merge', {
|
|
proposer: 'session-manager', source_domains: mergeInput.sourceDomains,
|
|
result_domains: mergeInput.resultDomains.map((domain) => ({
|
|
...domain, scope: domain.scope, not_scope: domain.not_scope,
|
|
owned_paths: domain.owned_paths, shared_paths: domain.shared_paths,
|
|
required_checks: domain.required_checks,
|
|
})),
|
|
ownership_assignments: mergeInput.ownershipAssignments.map((assignment) => ({
|
|
source_domain_id: assignment.sourceDomainId, source_path: assignment.sourcePath,
|
|
result_domain_id: assignment.resultDomainId,
|
|
})),
|
|
rationale: mergeInput.rationale,
|
|
expected_benefit: {
|
|
summary: benefit.summary, recurring_work: benefit.recurringWork,
|
|
context_locality: benefit.contextLocality, coordination_cost: benefit.coordinationCost,
|
|
},
|
|
evidence: mergeInput.evidence,
|
|
}, { actorId: 'session-manager', evolutionService: evolution });
|
|
assert.equal(merged.type, 'merge-domains');
|
|
assert.equal(merged.proposal.schema, 'torch.dev/fleet-boundary-proposal/v1alpha1');
|
|
evolution.approve({ changeId: merged.id, approvedBy: 'fixture-owner' });
|
|
const plan = evolution.planActivation(merged.id);
|
|
assert.equal(plan.canProceed, false);
|
|
assert.equal(plan.blockers.some((blocker) => blocker.type === 'manual-boundary-redesign-required'), true);
|
|
writeFileSync(configPath, `${before.config.trimEnd()} \n`);
|
|
assert.equal(evolution.planActivation(merged.id).blockers
|
|
.some((blocker) => blocker.type === 'configuration-changed'), true);
|
|
writeFileSync(configPath, before.config);
|
|
assert.throws(
|
|
() => evolution.activate({ changeId: merged.id, approvedBy: 'fixture-owner' }),
|
|
(error) => error.code === 'FLEET_BOUNDARY_MIGRATION_REQUIRED',
|
|
);
|
|
assert.equal(readFileSync(configPath, 'utf8'), before.config);
|
|
assert.equal(readFileSync(rosterPath, 'utf8'), before.roster);
|
|
assert.deepEqual(control.listAgents().map((agent) => agent.areaId).sort(), ['alpha', 'beta', 'session-manager']);
|
|
control.close();
|
|
|
|
const splitPath = join(context.root, 'split-proposal.json');
|
|
writeFileSync(splitPath, `${JSON.stringify({
|
|
sourceDomains: ['alpha'],
|
|
resultDomains: [
|
|
{
|
|
id: 'alpha-app', title: 'Alpha app', scope: ['application entrypoint'], not_scope: ['alpha internals'],
|
|
owned_paths: ['app.js'], shared_paths: [], neighbours: ['alpha-internals'], required_checks: [], resources: [], runtime: 'claude',
|
|
},
|
|
{
|
|
id: 'alpha-internals', title: 'Alpha internals', scope: ['alpha internals'], not_scope: ['application entrypoint'],
|
|
owned_paths: ['src/alpha/**'], shared_paths: [], neighbours: ['alpha-app'], required_checks: [], resources: [], runtime: 'claude',
|
|
},
|
|
],
|
|
ownershipAssignments: [
|
|
{ sourceDomainId: 'alpha', sourcePath: 'app.js', resultDomainId: 'alpha-app' },
|
|
{ sourceDomainId: 'alpha', sourcePath: 'src/alpha/**', resultDomainId: 'alpha-internals' },
|
|
],
|
|
rationale: 'Alpha now contains two recurring responsibilities with different context.',
|
|
expectedBenefit: benefit, evidence: ['TASK-44', 'ownership review'],
|
|
}, null, 2)}\n`);
|
|
const run = (args) => spawnSync(process.execPath, [CLI, ...args], {
|
|
cwd: context.root, env: context.env, encoding: 'utf8',
|
|
});
|
|
const proposed = run(['fleet', 'propose-split', '--from', 'session-manager', '--proposal', splitPath, '--json']);
|
|
assert.equal(proposed.status, 0, proposed.stderr || proposed.stdout);
|
|
const split = JSON.parse(proposed.stdout);
|
|
assert.equal(split.type, 'split-domain');
|
|
const refused = run(['fleet', 'reject', '--change', split.id, '--by', 'fixture-owner', '--reason', 'Wait for milestone.', '--json']);
|
|
assert.equal(refused.status, 2);
|
|
assert.equal(JSON.parse(refused.stdout).error, 'APPROVAL_REQUIRED');
|
|
const rejected = run(['fleet', 'reject', '--change', split.id, '--by', 'fixture-owner', '--reason', 'Wait for milestone.', '--yes', '--json']);
|
|
assert.equal(rejected.status, 0, rejected.stderr || rejected.stdout);
|
|
assert.equal(JSON.parse(rejected.stdout).state, 'rejected');
|
|
assert.equal(JSON.parse(rejected.stdout).rejectionReason, 'Wait for milestone.');
|
|
const reopened = openControlPlane({ repositoryRoot: context.root, env: context.env });
|
|
const persisted = new FleetEvolutionService({ repositoryRoot: context.root, controlPlane: reopened }).get(merged.id);
|
|
assert.equal(persisted.type, 'merge-domains');
|
|
assert.equal(persisted.proposal.resultDomains[0].id, 'platform');
|
|
assert.equal(new FleetEvolutionService({ repositoryRoot: context.root, controlPlane: reopened })
|
|
.get(split.id).rejectionReason, 'Wait for milestone.');
|
|
reopened.close();
|
|
});
|
|
|
|
test('SCN-fleet-evolution: the manager proposes and owner activates a newly justified persistent domain', () => {
|
|
const context = fixture();
|
|
const control = openControlPlane({ repositoryRoot: context.root, env: context.env });
|
|
const evolution = new FleetEvolutionService({
|
|
repositoryRoot: context.root,
|
|
controlPlane: control,
|
|
clock: () => new Date('2026-09-27T12:00:00Z'),
|
|
idFactory: () => 'CHANGE-1',
|
|
});
|
|
const worker = control.listAgents().find((agent) => agent.areaId !== 'session-manager').areaId;
|
|
const request = {
|
|
domain: {
|
|
id: 'payments', title: 'Payment systems', kind: 'development',
|
|
scope: ['payment intent lifecycle and settlement verification'],
|
|
not_scope: ['checkout presentation'], owned_paths: ['src/payments/**'],
|
|
shared_paths: ['src/contracts/payment-schema.js'], neighbours: [worker],
|
|
required_checks: [], resources: [], runtime: 'codex', model: 'gpt-test',
|
|
},
|
|
rationale: 'Repeated payment work has no coherent owner in the current Fleet.',
|
|
expectedBenefit: {
|
|
summary: 'Retain payment-domain context across recurring settlement work.',
|
|
recurringWork: 'Three queued milestones and ongoing incident ownership.',
|
|
contextLocality: 'Avoid reloading payment invariants into unrelated sessions.',
|
|
coordinationCost: 'One explicit API boundary with the existing core domain.',
|
|
},
|
|
evidence: ['docs/product-spec.md:42', 'TASK-17', 'TASK-23'],
|
|
};
|
|
assert.throws(
|
|
() => evolution.proposeDomain({ ...request, proposer: worker }),
|
|
(error) => error.code === 'FLEET_CHANGE_AUTHORITY_REQUIRED',
|
|
);
|
|
const proposed = callTorchTool(control, 'torch_propose_domain', {
|
|
proposer: 'session-manager', domain: request.domain, rationale: request.rationale,
|
|
expected_benefit: {
|
|
summary: request.expectedBenefit.summary,
|
|
recurring_work: request.expectedBenefit.recurringWork,
|
|
context_locality: request.expectedBenefit.contextLocality,
|
|
coordination_cost: request.expectedBenefit.coordinationCost,
|
|
},
|
|
evidence: request.evidence,
|
|
}, { actorId: 'session-manager', evolutionService: evolution });
|
|
assert.equal(proposed.state, 'proposed');
|
|
assert.equal(control.listAgents().some((agent) => agent.areaId === 'payments'), false);
|
|
assert.throws(
|
|
() => evolution.activate({ changeId: proposed.id, approvedBy: 'fixture-owner' }),
|
|
(error) => error.code === 'FLEET_CHANGE_AUTHORITY_REQUIRED',
|
|
);
|
|
const approved = evolution.approve({ changeId: proposed.id, approvedBy: 'fixture-owner' });
|
|
assert.equal(approved.state, 'approved');
|
|
const observed = observeProject({ repositoryRoot: context.root, env: context.env });
|
|
assert.equal(observed.fleetChanges[0].domainId, 'payments');
|
|
assert.equal(observed.fleetChanges[0].state, 'approved');
|
|
const plan = evolution.planActivation(proposed.id);
|
|
assert.equal(plan.canProceed, true, JSON.stringify(plan.blockers));
|
|
assert.equal(plan.mutationPerformed, false);
|
|
|
|
const activated = evolution.activate({ changeId: proposed.id, approvedBy: 'fixture-owner' });
|
|
assert.equal(activated.state, 'active');
|
|
assert.equal(activated.identity.areaId, 'payments');
|
|
assert.equal(activated.identity.state, 'offline');
|
|
assert.equal(activated.identity.model, 'gpt-test');
|
|
assert.equal(existsSync(activated.worktree), true);
|
|
assert.equal(existsSync(join(activated.worktree, '.torch', 'prompts', 'payments.md')), true);
|
|
assert.equal(execFileSync('git', ['-C', activated.worktree, 'status', '--porcelain'], { encoding: 'utf8' }), '');
|
|
assert.equal(execFileSync('git', ['-C', context.root, 'status', '--porcelain'], { encoding: 'utf8' }), '');
|
|
const roster = JSON.parse(readFileSync(join(context.root, '.torch', 'roster.yaml'), 'utf8'));
|
|
assert.equal(roster.areas.find((area) => area.id === worker).neighbours.includes('payments'), true);
|
|
assert.equal(roster.areas.find((area) => area.id === 'payments').neighbours.includes(worker), true);
|
|
assert.match(execFileSync('git', ['-C', context.root, 'log', '-1', '--pretty=%s'], { encoding: 'utf8' }), /add payments domain/);
|
|
const adapter = createCodexAdapter({ executable: '/opt/codex/bin/codex' });
|
|
const adapters = new Map([['codex', adapter]]);
|
|
const startPlan = planAreaUp({
|
|
repositoryRoot: context.root, controlPlane: control, areaId: 'payments', adapters, fresh: true,
|
|
});
|
|
assert.equal(startPlan.canProceed, true, JSON.stringify(startPlan.blockers));
|
|
assert.deepEqual(startPlan.actions.map((action) => action.areaId), ['payments']);
|
|
const started = startFleet({
|
|
plan: startPlan, controlPlane: control, adapters,
|
|
executor: () => ({
|
|
status: 0, stdout: '{"type":"thread.started","thread_id":"payments-thread"}\n{"type":"turn.completed"}\n',
|
|
}),
|
|
});
|
|
assert.equal(started.started[0].runtimeSessionId, 'payments-thread');
|
|
assert.equal(control.identity('payments').state, 'idle');
|
|
control.close();
|
|
|
|
const reopened = openControlPlane({ repositoryRoot: context.root, env: context.env });
|
|
assert.equal(reopened.identity('payments').areaId, 'payments');
|
|
const persisted = new FleetEvolutionService({ repositoryRoot: context.root, controlPlane: reopened }).get(proposed.id);
|
|
assert.equal(persisted.activationCommit, activated.activationCommit);
|
|
reopened.close();
|
|
});
|
|
|
|
test('SCN-cli-fleet-evolution: public CLI separates manager proposal, owner approval, plan, and activation', () => {
|
|
const context = fixture();
|
|
const inputPath = join(context.root, 'domain-change.json');
|
|
writeFileSync(inputPath, `${JSON.stringify({
|
|
domain: {
|
|
id: 'security', title: 'Security engineering', scope: ['threat modeling and security checks'],
|
|
not_scope: ['general feature implementation'], owned_paths: ['security/**'],
|
|
shared_paths: [], neighbours: [], required_checks: [], runtime: 'claude',
|
|
},
|
|
rationale: 'Recurring threat-model and audit work lacks a durable owner.',
|
|
expectedBenefit: {
|
|
summary: 'Keep security assumptions focused and reviewable.',
|
|
recurringWork: 'Security review recurs at each release.',
|
|
contextLocality: 'Retain threat models instead of reloading them into feature sessions.',
|
|
coordinationCost: 'The Session Manager coordinates explicit review requests.',
|
|
},
|
|
evidence: ['docs/security-model.md:1', 'release policy'],
|
|
}, null, 2)}\n`);
|
|
execFileSync('git', ['-C', context.root, 'add', 'domain-change.json']);
|
|
execFileSync('git', ['-C', context.root, 'commit', '-m', 'add domain proposal input']);
|
|
const run = (args) => spawnSync(process.execPath, [CLI, ...args], {
|
|
cwd: context.root, env: context.env, encoding: 'utf8',
|
|
});
|
|
const proposedResult = run(['fleet', 'propose', '--from', 'session-manager', '--proposal', inputPath, '--json']);
|
|
assert.equal(proposedResult.status, 0, proposedResult.stderr || proposedResult.stdout);
|
|
const proposed = JSON.parse(proposedResult.stdout);
|
|
const refused = run(['fleet', 'approve', '--change', proposed.id, '--by', 'fixture-owner', '--json']);
|
|
assert.equal(refused.status, 2);
|
|
assert.equal(JSON.parse(refused.stdout).error, 'APPROVAL_REQUIRED');
|
|
assert.equal(run(['fleet', 'approve', '--change', proposed.id, '--by', 'fixture-owner', '--yes', '--json']).status, 0);
|
|
const plan = run(['fleet', 'plan', '--change', proposed.id, '--json']);
|
|
assert.equal(plan.status, 0, plan.stderr || plan.stdout);
|
|
assert.equal(JSON.parse(plan.stdout).canProceed, true);
|
|
const activated = run(['fleet', 'activate', '--change', proposed.id, '--by', 'fixture-owner', '--yes', '--json']);
|
|
assert.equal(activated.status, 0, activated.stderr || activated.stdout);
|
|
assert.equal(JSON.parse(activated.stdout).identity.areaId, 'security');
|
|
const startPlan = run(['fleet', 'start', '--change', proposed.id, '--fresh', '--dry-run', '--json']);
|
|
assert.equal(startPlan.status, 0, startPlan.stderr || startPlan.stdout);
|
|
assert.deepEqual(JSON.parse(startPlan.stdout).actions.map((action) => action.areaId), ['security']);
|
|
assert.equal(run(['agent', '--area', 'security', '--json']).status, 0);
|
|
});
|
|
|
|
test('SCN-fleet-retirement: owner-approved retirement shrinks the roster without deleting branch history', () => {
|
|
const context = fixture();
|
|
const control = openControlPlane({ repositoryRoot: context.root, env: context.env });
|
|
let sequence = 0;
|
|
const evolution = new FleetEvolutionService({
|
|
repositoryRoot: context.root, controlPlane: control, idFactory: () => `CHANGE-${++sequence}`,
|
|
});
|
|
const existing = control.listAgents().find((agent) => agent.areaId !== 'session-manager').areaId;
|
|
const addition = evolution.proposeDomain({
|
|
proposer: 'session-manager',
|
|
domain: {
|
|
id: 'review', title: 'Review', scope: ['cross-boundary review'], not_scope: ['feature implementation'],
|
|
owned_paths: ['review/**'], shared_paths: [], neighbours: [existing], required_checks: [], resources: [], runtime: 'claude',
|
|
},
|
|
rationale: 'Milestone reviews recur without a coherent owner.',
|
|
expectedBenefit: {
|
|
summary: 'Keep a recurring review boundary focused.', recurringWork: 'Review work recurs each milestone.',
|
|
contextLocality: 'Retain the review assumptions in one session.', coordinationCost: 'One explicit handoff.',
|
|
},
|
|
evidence: ['TASK-9'],
|
|
});
|
|
evolution.approve({ changeId: addition.id, approvedBy: 'fixture-owner' });
|
|
const activated = evolution.activate({ changeId: addition.id, approvedBy: 'fixture-owner' });
|
|
assert.equal(existsSync(activated.worktree), true);
|
|
|
|
const retirement = callTorchTool(control, 'torch_propose_domain_retirement', {
|
|
area_id: 'review',
|
|
rationale: 'The review stream ended and a permanent session now costs more coordination than it saves.',
|
|
expected_benefit: {
|
|
summary: 'Shrink routine coordination while preserving review history.',
|
|
recurring_work: 'No recurring review work remains after the milestone.',
|
|
context_locality: 'Future isolated reviews can use bounded temporary help.',
|
|
coordination_cost: 'Removes an idle persistent handoff boundary.',
|
|
},
|
|
evidence: ['TASK-9 completed', 'decision: review milestone closed'],
|
|
}, { actorId: 'session-manager', evolutionService: evolution });
|
|
assert.equal(retirement.type, 'retire-domain');
|
|
evolution.approve({ changeId: retirement.id, approvedBy: 'fixture-owner' });
|
|
|
|
control.reportStatus({ areaId: 'review', state: 'idle', summary: 'Awaiting retirement.' });
|
|
assert.equal(evolution.planActivation(retirement.id).blockers
|
|
.some((blocker) => blocker.type === 'runtime-not-offline'), true);
|
|
control.reportStatus({ areaId: 'review', state: 'offline', summary: 'Stopped.' });
|
|
writeFileSync(join(activated.worktree, 'dirty.txt'), 'preserve me\n');
|
|
assert.equal(evolution.planActivation(retirement.id).blockers
|
|
.some((blocker) => blocker.type === 'worktree-unsafe' && blocker.problem === 'worktree-dirty'), true);
|
|
execFileSync('git', ['-C', activated.worktree, 'clean', '-f', '--', 'dirty.txt']);
|
|
|
|
const plan = evolution.planActivation(retirement.id);
|
|
assert.equal(plan.canProceed, true, JSON.stringify(plan.blockers));
|
|
const retired = evolution.activate({ changeId: retirement.id, approvedBy: 'fixture-owner' });
|
|
assert.equal(retired.state, 'retired');
|
|
assert.equal(existsSync(activated.worktree), false);
|
|
assert.notEqual(execFileSync('git', ['-C', context.root, 'branch', '--list', activated.branch], { encoding: 'utf8' }).trim(), '');
|
|
assert.equal(control.listAgents().some((agent) => agent.areaId === 'review'), false);
|
|
const config = JSON.parse(readFileSync(join(context.root, '.torch', 'torch.yaml'), 'utf8'));
|
|
assert.equal(config.domains.some((domain) => domain.id === 'review'), false);
|
|
assert.equal(config.retired_domains.some((domain) => domain.id === 'review' && domain.branch === activated.branch), true);
|
|
assert.match(execFileSync('git', ['-C', context.root, 'log', '-1', '--pretty=%s'], { encoding: 'utf8' }), /retire review domain/);
|
|
control.close();
|
|
});
|