Files
qinglong/packages/ql3-local-application/test/legacyDataApplicationCommitment.test.cjs

205 lines
6.5 KiB
JavaScript

const assert = require('node:assert/strict');
const crypto = require('node:crypto');
const fs = require('node:fs');
const os = require('node:os');
const path = require('node:path');
const { test } = require('node:test');
const {
createLocalDataDirectoryApplicationCommit,
} = require('@qinglong/local-sqlite/data-directory-application-commit');
const {
LOCAL_APPLICATION_PROCESS_CONFIG_SCHEMA_V3,
LOCAL_APPLICATION_PROCESS_CONFIG_SCHEMA_V4,
LocalApplicationProcessConfigError,
normalizeLocalApplicationProcessConfig,
} = require('../dist/production-process/processConfig.js');
const {
LocalApplicationLegacyDataCommitmentError,
verifyLocalApplicationLegacyDataCommitment,
} = require('../dist/production-process/legacyDataApplicationCommitment.js');
const {
runProductionLocalApplicationProcess,
} = require('../dist/production-process/processApplication.js');
function temporaryRoot(t) {
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'ql3-data-commitment-'));
t.after(() => fs.rmSync(root, { recursive: true, force: true }));
return root;
}
function commitDocument(profile = 'edge') {
return createLocalDataDirectoryApplicationCommit({
mutationId: '00000000-0000-4000-8000-000000000001',
projectId: 'project-edge-1',
profile,
sourceStageManifestDigest: '1'.repeat(64),
transformationDigest: '2'.repeat(64),
modelDigest: '3'.repeat(64),
publicationDigest: '4'.repeat(64),
receiptDigest: '5'.repeat(64),
committedAtMs: 1_000,
receipt: {
secretCount: 2,
environmentSecretCount: 1,
sshSecretCount: 1,
},
});
}
function v4Config(root, commit) {
const cutoverPayload = {
schemaVersion: 1,
kind: 'qinglong3-local-legacy-silence-commitment',
state: 'legacy_stopped',
cutoverId: 'cutover-data-1',
profile: 'edge',
instanceId: 'edge-router-1',
activationDigest: 'a'.repeat(64),
previousRecordDigest: 'b'.repeat(64),
requestedAtMs: 1_000,
observedAtMs: 1_000,
controller: {
kind: 'docker',
endpointDigest: 'c'.repeat(64),
legacyContainerId: 'd'.repeat(64),
legacyContainerIdentityDigest: 'e'.repeat(64),
legacySourceBindingDigest: 'f'.repeat(64),
},
};
return {
schema: LOCAL_APPLICATION_PROCESS_CONFIG_SCHEMA_V4,
instanceId: 'edge-router-1',
profile: 'edge',
storage: {
mode: 'adopted',
sourcePath: path.join(root, 'legacy.sqlite'),
targetPath: path.join(root, 'qinglong3.sqlite'),
recoveryPath: path.join(root, 'recovery.sqlite'),
manifestPath: path.join(root, 'manifest.json'),
activationPath: path.join(root, 'activation.json'),
expectedActivationDigest: 'a'.repeat(64),
busyTimeoutMs: 100,
},
runtime: {
receiptRoot: path.join(root, 'receipts'),
artifactRoot: path.join(root, 'artifacts'),
secretKeyringPath: path.join(root, 'secret-keyring.json'),
},
pluginPackages: {
stagingRoot: path.join(root, 'plugin-staging'),
activationRoot: path.join(root, 'plugin-activation'),
recoverySource: { mode: 'disabled' },
pageSize: 4,
maxPages: 4,
taskPublicationPageSize: 4,
taskPublicationMaxPages: 4,
},
ai: { deployment: 'excluded' },
cutover: {
cutoverId: cutoverPayload.cutoverId,
commitmentPath: path.join(root, 'legacy-stopped.json'),
expectedCommitmentDigest: crypto
.createHash('sha256')
.update(JSON.stringify(cutoverPayload), 'utf8')
.digest('hex'),
},
legacyDataApplication: {
commitPath: path.join(root, 'commit.json'),
expectedCommitDigest: commit.commitDigest,
expectedReceiptDigest: commit.receiptDigest,
},
};
}
function writeCommit(config, commit) {
fs.writeFileSync(
config.legacyDataApplication.commitPath,
`${JSON.stringify(commit)}\n`,
{ mode: 0o600 },
);
}
test('v4 config binds and verifies a canonical committed data application', (t) => {
const root = temporaryRoot(t);
const commit = commitDocument();
const config = v4Config(root, commit);
writeCommit(config, commit);
const normalized = normalizeLocalApplicationProcessConfig(config);
assert.equal(normalized.schema, LOCAL_APPLICATION_PROCESS_CONFIG_SCHEMA_V4);
assert.deepEqual(
verifyLocalApplicationLegacyDataCommitment(normalized),
commit,
);
});
test('v4 config rejects fresh storage and missing receipt binding', (t) => {
const root = temporaryRoot(t);
const commit = commitDocument();
const config = v4Config(root, commit);
assert.throws(
() =>
normalizeLocalApplicationProcessConfig({
...config,
storage: {
mode: 'fresh',
databasePath: path.join(root, 'fresh.sqlite'),
},
}),
LocalApplicationProcessConfigError,
);
const { legacyDataApplication, ...missing } = config;
assert.ok(legacyDataApplication);
assert.throws(
() => normalizeLocalApplicationProcessConfig(missing),
LocalApplicationProcessConfigError,
);
});
test('commit and receipt drift fail before signal or storage authority', async (t) => {
const root = temporaryRoot(t);
const commit = commitDocument();
const config = v4Config(root, commit);
writeCommit(config, commit);
config.legacyDataApplication.expectedReceiptDigest = '0'.repeat(64);
const configPath = path.join(root, 'local-application.json');
fs.writeFileSync(configPath, `${JSON.stringify(config)}\n`, { mode: 0o600 });
let subscriptions = 0;
let starts = 0;
await assert.rejects(
() =>
runProductionLocalApplicationProcess({
configFilePath: configPath,
signals: {
subscribe() {
subscriptions += 1;
return () => undefined;
},
},
emit() {},
async start() {
starts += 1;
throw new Error('must not start');
},
}),
(error) =>
error instanceof LocalApplicationLegacyDataCommitmentError &&
error.code === 'QL3_LOCAL_APPLICATION_LEGACY_DATA_COMMITMENT_INVALID',
);
assert.equal(subscriptions, 0);
assert.equal(starts, 0);
});
test('v3 remains SQLite-only and does not claim a data application receipt', (t) => {
const root = temporaryRoot(t);
const commit = commitDocument();
const config = v4Config(root, commit);
const { legacyDataApplication, ...withoutDataApplication } = config;
assert.ok(legacyDataApplication);
const v3 = normalizeLocalApplicationProcessConfig({
...withoutDataApplication,
schema: LOCAL_APPLICATION_PROCESS_CONFIG_SCHEMA_V3,
});
assert.equal(verifyLocalApplicationLegacyDataCommitment(v3), undefined);
});