69 lines
3.4 KiB
TypeScript
69 lines
3.4 KiB
TypeScript
/**
|
|
* Regression guard: the restore redeploy MUST force-recreate.
|
|
*
|
|
* redeployStackFromDir() is the redeploy-from-restored-snapshot path - its ONLY
|
|
* caller is the backup restore port (backups/index.ts), used by both the in-place
|
|
* and clone restore paths. A restore rewrites the stack dir and swaps the
|
|
* volume data underneath the stack, then this function runs `docker compose up`. A
|
|
* plain `up` sees the unchanged compose and only restarts the container compose
|
|
* thinks is up-to-date, which after an in-place swap can leave it not-yet-running
|
|
* (the source of an intermittent post-restore "stack never came up"). Passing
|
|
* forceRecreate rebuilds the container fresh against the restored state.
|
|
*
|
|
* This asserts at the source level (like tests/deploy-stack-online.test.ts) that the
|
|
* executeComposeCommand('up', ...) inside redeployStackFromDir carries
|
|
* forceRecreate: true - a real daemon isn't needed to prove the invariant, and it
|
|
* survives refactors of the surrounding code.
|
|
*/
|
|
// @ts-expect-error -- bun:test is a runtime built-in with no types installed
|
|
import { describe, test, expect } from 'bun:test';
|
|
import { readFileSync } from 'node:fs';
|
|
import { fileURLToPath } from 'node:url';
|
|
import { dirname, join } from 'node:path';
|
|
|
|
const here = dirname(fileURLToPath(import.meta.url));
|
|
const stacksSrc = readFileSync(join(here, '..', 'src', 'lib', 'server', 'stacks.ts'), 'utf8');
|
|
|
|
// The body of redeployStackFromDir, from its declaration to the next top-level
|
|
// `export ... function` (paren/brace matching would be overkill - the next export
|
|
// is a hard boundary).
|
|
function redeployStackFromDirBody(source: string): string {
|
|
const start = source.indexOf('export async function redeployStackFromDir');
|
|
expect(start).toBeGreaterThan(-1);
|
|
const rest = source.slice(start + 1);
|
|
const nextExport = rest.indexOf('\nexport ');
|
|
return rest.slice(0, nextExport === -1 ? undefined : nextExport);
|
|
}
|
|
|
|
describe('restore redeploy force-recreates', () => {
|
|
const body = redeployStackFromDirBody(stacksSrc);
|
|
|
|
test('redeployStackFromDir runs an `up`', () => {
|
|
expect(body).toContain("executeComposeCommand(");
|
|
expect(body).toContain("'up'");
|
|
});
|
|
|
|
test('redeployStackFromDir passes forceRecreate: true', () => {
|
|
// Whitespace-tolerant: `forceRecreate: true` in the options object.
|
|
expect(body).toMatch(/forceRecreate\s*:\s*true/);
|
|
});
|
|
|
|
// A restore loads raw DB secrets; if this path skips the provider-resolution choke
|
|
// point, a secret-marked op://... reaches the container as the literal reference
|
|
// string. Guard the wiring at the source level (a real behavioral test needs a
|
|
// local dev server + a fake provider CLI, which the CI shard can't host): the
|
|
// resolver must be CALLED, its result ASSIGNED BACK to the vars, and it must run
|
|
// BEFORE the deploy - so a refactor that drops the result or reorders the call fails.
|
|
test('redeployStackFromDir resolves provider references and uses the result before deploying', () => {
|
|
const call = body.indexOf('resolveProviderEnvVars(');
|
|
const deploy = body.indexOf('executeComposeCommand(');
|
|
expect(call).toBeGreaterThan(-1);
|
|
expect(deploy).toBeGreaterThan(-1);
|
|
// resolution happens before the compose command runs
|
|
expect(call).toBeLessThan(deploy);
|
|
// the resolved maps are assigned back (result not discarded)
|
|
expect(body).toMatch(/=\s*resolved\.dbNonSecretVars/);
|
|
expect(body).toMatch(/=\s*resolved\.secretVars/);
|
|
});
|
|
});
|