diff --git a/packages/bus/tests/copy-revoke.test.mjs b/packages/bus/tests/copy-revoke.test.mjs index a0aa4ecac..40356a836 100644 --- a/packages/bus/tests/copy-revoke.test.mjs +++ b/packages/bus/tests/copy-revoke.test.mjs @@ -9,7 +9,7 @@ import test from 'node:test'; import assert from 'node:assert/strict'; import { chmodSync, existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, unlinkSync, writeFileSync } from 'node:fs'; import { tmpdir } from 'node:os'; -import { join } from 'node:path'; +import { dirname, join } from 'node:path'; import { Broker, BusError } from '../src/broker.mjs'; import { Store } from '../src/store.mjs'; import { Credentials } from '../src/credentials.mjs'; @@ -569,6 +569,16 @@ test('CB4 archive with the managed adapter wired revokes the token at Gitea', as assert.deepEqual(f.bot.tokens, []); }); +test('CB4 archive refuses when a slot parent is not the stack user\'s private directory, and the slot stays', async (t) => { + const w = await world(t); + const { f } = await live(t, w); + chmodSync(dirname(f.slot), 0o755); + const revision = (await listOrgs(f.ctx)).find((o) => o.id === f.id).revision; + await assert.rejects(orgService.archiveOrg(f.ctx, f.id, { ifRevision: revision, busStop: async () => ({ stopped: true, reason: 'stopped' }) }), refuses('credential-revoke-failed')); + assert.ok(existsSync(f.slot)); + assert.equal(f.rec().state, 'revoking'); +}); + test('CB4 archive rerun after a crash joins the pending op', async (t) => { const w = await world(t); const { f } = await live(t, w); @@ -684,6 +694,17 @@ test('CB4 a refused begin reopens the gate only for an owner verb on an open cop assert.deepEqual(await run({ cause: 'revoke', closed: false }), [['close', 'rev', null], ['open', 'rev', true]]); }); +test('CB4 resume keeps a failed pass closed with its failure code while it runs again', async () => { + const calls = []; + const gate = { closed: () => true, close: (b, i, c) => calls.push([i, c ?? null]), open: () => calls.push(['open']), drain: async () => {} }; + const copyBindings = { rev: { state: 'revoking', revoke: { id: 'op-1', cause: 'expired', at: 'x', failed: ['remote'] } }, two: { state: 'revoking', revoke: { id: 'op-2', cause: 'expired', at: 'x' } } }; + const writer = { read: () => ({ document: { copyBindings } }), revokeCopyBinding: () => { throw new BusError('org-mismatch'); } }; + const revoker = copyRevoker({ writer, gate, credentials: { discardCopy: () => 'absent' }, stopRuns: async () => ({ confirmed: true }) }); + await revoker.resume('acme'); + assert.deepEqual(calls.slice(0, 2), [['rev', 'credential-revoke-failed'], ['two', null]]); + assert.ok(!calls.some((c) => c[0] === 'open')); +}); + test('CB4 a second organization launches and calls throughout the first one\'s revoke, archive and delete', async (t) => { const w = await world(t); const { f: demo, r: dr } = await live(t, w); diff --git a/packages/cli/tests/org-restore-bus.test.mjs b/packages/cli/tests/org-restore-bus.test.mjs index aa94072a4..c9349eb2d 100644 --- a/packages/cli/tests/org-restore-bus.test.mjs +++ b/packages/cli/tests/org-restore-bus.test.mjs @@ -92,3 +92,24 @@ test('E1: an agent run may restore only with --no-bus', async t => { assert.equal(starts, 0); assert.equal(await f.state(), 'active'); }); + +test('CB4 archive and delete from the CLI hand the managed copy remote to the organization service', async t => { + const home = mkdtempSync(join(tmpdir(), 'cb4-cli-')), saved = process.env.HOME; + process.env.HOME = home; + t.after(() => { process.env.HOME = saved; rmSync(home, { recursive: true, force: true }); }); + const configDir = join(home, '.config', 'mosaic-e1test'), dataRoot = join(home, 'data'); + mkdirSync(configDir, { recursive: true, mode: 0o700 }); mkdirSync(dataRoot, { mode: 0o700 }); + const ctx = { configDir, dataRoot, instance: 'e1test', principal: DEV_OWNER }; + await service.createOrg(ctx, 'alpha', await service.templateRender('alpha')); + const env = humanEnv({ HOME: home, MOSAIC_CONFIG: join(configDir, 'config.json') }); + const asked = []; + const copyRemoteFor = async c => { asked.push([c.configDir, c.dataRoot]); return async () => 'revoked'; }; + const run = words => orgCommand(words, io(env), { system: { dataRoot }, copyRemoteFor }); + const revision = (await listOrgs(ctx)).find(e => e.id === 'alpha').revision; + await run(['archive', 'alpha', '--if-revision', String(revision), '--json']); + assert.deepEqual(asked, [[configDir, dataRoot]]); + const archived = (await listOrgs(ctx)).find(e => e.id === 'alpha').revision; + await run(['delete', 'alpha', '--confirm', 'alpha', '--if-revision', String(archived), '--json']); + assert.deepEqual(asked, [[configDir, dataRoot], [configDir, dataRoot]]); + assert.equal((await listOrgs(ctx)).find(e => e.id === 'alpha')?.state ?? 'deleted', 'deleted'); +});