Ctrl K
ComunidadGuías…

scripts/stack-plan.test.mjs

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
import assert from 'node:assert/strict';
import { spawnSync } from 'node:child_process';
import { fileURLToPath } from 'node:url';
import test from 'node:test';

const planner = fileURLToPath(new URL('./stack-volumes.jq', import.meta.url));
const day = 86400;
const now = 1800000000;
const stamp = seconds => new Date(seconds * 1000).toISOString();
const project = 'mike-test';
const volume = (name = 'data', labels = {}) => ({ Name: name, CreatedAt: stamp(now - 30 * day),
  Labels: { 'com.docker.compose.project': project, ...labels } });
const container = (status = 'exited', finished = now - 60) => ({ Id: 'db',
  Config: { Labels: { 'com.docker.compose.project': project } }, Created: stamp(now - 30 * day),
  State: { Status: status, StartedAt: stamp(now - 20 * day), FinishedAt: stamp(finished) },
  Mounts: [{ Type: 'volume', Name: 'data' }] });
const record = (idle = now - 60, v = volume()) => ({ version: 1, volumes: { [v.Name]: {
  created: v.CreatedAt, project, supabase: '', idle_since: idle,
} } });
function plan({ cs = [container()], vs = [volume()], previous = { version: 1, volumes: {} },
  kind = 'mike', scope = true, time = now, age = 7 * day, only = 'mike-*', allowed = '' } = {}) {
  const args = ['-n'];
  for (const [key, value] of Object.entries({ containers: [cs], volumes: [vs], previous: [previous], now: time, age })) {
    args.push('--argjson', key, JSON.stringify(value));
  }
  args.push('--arg', 'classifications', `${project}\t${kind}\t${scope}\n`,
    '--arg', 'only', only, '--arg', 'allowed', allowed, '-f', planner);
  const result = spawnSync('jq', args, { encoding: 'utf8' });
  assert.equal(result.status, 0, result.stderr);
  return JSON.parse(result.stdout);
}

for (const status of ['running', 'paused', 'restarting', 'removing', 'unknown']) {
  test(`protects ${status} containers and declared unmounted volumes`, () => {
    const p = plan({ cs: [container(status)], vs: [volume(), volume('unused')], age: 0 });
    assert.deepEqual(p.containers, []);
    assert.deepEqual(p.remove, []);
    assert.deepEqual(p.protected, [project]);
  });
}
for (const status of ['exited', 'dead', 'created']) {
  test(`removes ${status} containers but keeps recently used data`, () => {
    const p = plan({ cs: [container(status)] });
    assert.deepEqual(p.containers, ['db']);
    assert.deepEqual(p.remove, []);
  });
}
test('second sweep retains recent use; data expires only after the idle threshold', () => {
  const first = plan();
  const second = plan({ cs: [], kind: 'unknown', previous: first.state, time: now + 1 });
  assert.deepEqual(second.remove, []);
  const expired = plan({ cs: [], kind: 'unknown', previous: second.state, time: now + 8 * day });
  assert.deepEqual(expired.remove, ['data']);
});
test('known foreign project vetoes a saved record and a mike-prefixed name', () => {
  const foreign = { ...container(), Mounts: [] };
  const p = plan({ cs: [foreign], kind: 'other', previous: record(now - 30 * day), age: 0 });
  assert.deepEqual(p.containers, []);
  assert.deepEqual(p.remove, []);
  assert.deepEqual(p.state.volumes, {});
});
test('unknown named volumes are kept even with a mike project prefix', () => {
  assert.deepEqual(plan({ cs: [], kind: 'unknown', age: 0 }).remove, []);
});
test('preview includes a volume attached only to an eligible stopped stack', () => {
  const p = plan({ cs: [container('exited', now - 10 * day)] });
  assert.deepEqual(p.containers, ['db']);
  assert.deepEqual(p.remove, ['data']);
});
test('a shared volume still referenced by a non-Compose container is kept', () => {
  const external = { ...container('exited'), Id: 'foreign', Config: { Labels: {} } };
  const p = plan({ cs: [container('exited', now - 10 * day), external] });
  assert.deepEqual(p.containers, ['db']);
  assert.deepEqual(p.remove, []);
});
test('recreated volume cannot inherit the previous incarnation idle time', () => {
  const p = plan({ cs: [], kind: 'unknown', previous: record(now - 30 * day),
    vs: [{ ...volume(), CreatedAt: stamp(now) }] });
  assert.deepEqual(p.remove, []);
});
test('reuse refreshes the saved idle time before a later sweep', () => {
  const p = plan({ cs: [container('running')], previous: record(now - 30 * day) });
  assert.equal(p.state.volumes.data.idle_since, null);
  assert.deepEqual(plan({ cs: [], kind: 'unknown', previous: p.state, time: now + 30 * day }).remove, []);
});
test('lost state fails closed for unknown named volumes', () => {
  assert.deepEqual(plan({ cs: [], kind: 'unknown' }).remove, []);
});
test('out-of-scope projects retain records but schedule no operations', () => {
  const old = record(now - 30 * day);
  const p = plan({ scope: false, previous: old, age: 0 });
  assert.deepEqual(p.remove, []);
  assert.deepEqual(p.containers, []);
  assert.deepEqual(p.state, old);
});
test('anonymous orphans get an observation grace period and respect MIKE_GC_ONLY', () => {
  const v = volume('anon', { 'com.docker.compose.project': null, 'com.docker.volume.anonymous': '' });
  const first = plan({ cs: [], vs: [v], only: '' });
  assert.deepEqual(first.remove, []);
  assert.deepEqual(plan({ cs: [], vs: [v], only: '', previous: first.state, time: now + 8 * day }).remove, ['anon']);
  assert.deepEqual(plan({ cs: [], vs: [v], age: 0 }).remove, []);
});
test('Supabase orphan grace period respects the explicit allowlist', () => {
  const v = volume('data', { 'com.supabase.cli.project': project });
  const first = plan({ cs: [], vs: [v], kind: 'unknown', allowed: project });
  assert.deepEqual(first.remove, []);
  assert.deepEqual(plan({ cs: [], vs: [v], kind: 'unknown', allowed: project,
    previous: first.state, time: now + 8 * day }).remove, ['data']);
  assert.deepEqual(plan({ cs: [], vs: [v], kind: 'unknown', previous: first.state, age: 0 }).remove, []);
});
test('unparseable activity timestamps keep data conservatively', () => {
  const c = container('exited', now - 20 * day);
  c.State.FinishedAt = 'invalid';
  assert.deepEqual(plan({ cs: [c] }).remove, []);
});