mirror of
https://github.com/whyour/qinglong.git
synced 2026-09-23 03:18:09 +08:00
feat(ql3): gate startup on legacy data receipt
This commit is contained in:
@@ -0,0 +1,204 @@
|
||||
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);
|
||||
});
|
||||
Reference in New Issue
Block a user