mirror of
https://github.com/whyour/qinglong.git
synced 2026-09-23 03:18:09 +08:00
feat(local): publish secret config reconciliation plans
This commit is contained in:
@@ -27,7 +27,8 @@ const HEADER = Object.freeze({
|
||||
bundleFingerprintDigest: '1'.repeat(64),
|
||||
profile: 'edge',
|
||||
projectId: 'project-1',
|
||||
tableDisposition: 'adopt_legacy',
|
||||
tableDisposition: 'manual_external',
|
||||
unadaptedLegacyConfigCount: 0,
|
||||
preparedHeadDigest: '2'.repeat(64),
|
||||
preparedAtMs: 1_780_000_000_000,
|
||||
});
|
||||
@@ -60,11 +61,60 @@ function databases() {
|
||||
created_at_ms INTEGER NOT NULL,
|
||||
PRIMARY KEY (project_id, secret_name, version)
|
||||
);
|
||||
CREATE TABLE "QingLong3LegacyAdoptions" (
|
||||
mutation_id TEXT PRIMARY KEY,
|
||||
decision_id TEXT NOT NULL,
|
||||
project_id TEXT NOT NULL,
|
||||
plan_digest TEXT NOT NULL,
|
||||
inventory_digest TEXT NOT NULL,
|
||||
decision_digest TEXT NOT NULL,
|
||||
receipt_digest TEXT NOT NULL,
|
||||
authorization_file_digest TEXT NOT NULL,
|
||||
publication_digest TEXT NOT NULL,
|
||||
row_count INTEGER NOT NULL,
|
||||
adopted_task_count INTEGER NOT NULL,
|
||||
adopted_trigger_count INTEGER NOT NULL,
|
||||
skipped_count INTEGER NOT NULL,
|
||||
audit_event_id TEXT NOT NULL,
|
||||
created_at_ms INTEGER NOT NULL
|
||||
);
|
||||
`);
|
||||
return { legacy, target };
|
||||
}
|
||||
|
||||
function writePlan(t, legacy, target, maxBytes = 8 * 1024 * 1024) {
|
||||
function insertAutomationAdoption(target, adoptedTaskCount = 1) {
|
||||
const mutationId = '30000000-0000-4000-8000-000000000003';
|
||||
target
|
||||
.prepare(
|
||||
`INSERT INTO "QingLong3LegacyAdoptions" VALUES
|
||||
(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
||||
)
|
||||
.run(
|
||||
mutationId,
|
||||
'019b0000-0000-7000-8000-000000000001',
|
||||
HEADER.projectId,
|
||||
'3'.repeat(64),
|
||||
'4'.repeat(64),
|
||||
'5'.repeat(64),
|
||||
'6'.repeat(64),
|
||||
'7'.repeat(64),
|
||||
'8'.repeat(64),
|
||||
adoptedTaskCount,
|
||||
adoptedTaskCount,
|
||||
0,
|
||||
0,
|
||||
mutationId,
|
||||
HEADER.preparedAtMs,
|
||||
);
|
||||
}
|
||||
|
||||
function writePlan(
|
||||
t,
|
||||
legacy,
|
||||
target,
|
||||
maxBytes = 8 * 1024 * 1024,
|
||||
header = HEADER,
|
||||
) {
|
||||
const directory = fs.mkdtempSync(path.join(os.tmpdir(), 'ql3-secret-plan-'));
|
||||
t.after(() => fs.rmSync(directory, { recursive: true, force: true }));
|
||||
const filePath = path.join(directory, 'plan.ndjson');
|
||||
@@ -74,7 +124,7 @@ function writePlan(t, legacy, target, maxBytes = 8 * 1024 * 1024) {
|
||||
result = writeLocalReconciliationSecretConfigPlan({
|
||||
descriptor,
|
||||
maxBytes,
|
||||
header: HEADER,
|
||||
header,
|
||||
legacy,
|
||||
target,
|
||||
});
|
||||
@@ -107,6 +157,7 @@ test('writes a content-free Env plan with separate active and disabled candidate
|
||||
(2, 'TOKEN', 'pinned-secret', 0, 1, 1, '2026-01-02'),
|
||||
(3, 'DISABLED_TOKEN', 'disabled-secret', 1, 0, 0, '2026-01-03');
|
||||
`);
|
||||
insertAutomationAdoption(target);
|
||||
|
||||
const { result, records, serialized } = writePlan(t, legacy, target);
|
||||
assert.equal(result.footer.outcome, 'ready');
|
||||
@@ -114,6 +165,9 @@ test('writes a content-free Env plan with separate active and disabled candidate
|
||||
assert.equal(result.footer.eligibleBindingCount, 1);
|
||||
assert.equal(result.footer.eligiblePreservationCount, 1);
|
||||
assert.equal(result.footer.targetConflictCount, 0);
|
||||
assert.equal(result.footer.automationAdoptionRecordCount, 1);
|
||||
assert.equal(result.footer.adoptedLegacyTaskCount, 1);
|
||||
assert.match(result.footer.automationAdoptionSetDigest, /^[0-9a-f]{64}$/);
|
||||
const candidates = records.filter((record) =>
|
||||
record.kind.endsWith('-candidate'),
|
||||
);
|
||||
@@ -234,6 +288,28 @@ test('makes absent Envs no-effect and malformed Env manual', (t) => {
|
||||
assert.equal(manual.serialized.includes('private-value'), false);
|
||||
});
|
||||
|
||||
test('keeps active Env and historical Configs manual without adoption authority', (t) => {
|
||||
const { legacy, target } = databases();
|
||||
t.after(() => legacy.close());
|
||||
t.after(() => target.close());
|
||||
legacy.exec(
|
||||
`INSERT INTO "Envs" VALUES
|
||||
(1, 'TOKEN', 'private-value', 0, 1, 0, '2026-01-01')`,
|
||||
);
|
||||
const withoutAdoption = writePlan(t, legacy, target);
|
||||
assert.equal(withoutAdoption.result.footer.outcome, 'manual_required');
|
||||
assert.equal(withoutAdoption.result.footer.adoptedLegacyTaskCount, 0);
|
||||
assert.equal(withoutAdoption.serialized.includes('private-value'), false);
|
||||
|
||||
insertAutomationAdoption(target);
|
||||
const withConfigs = writePlan(t, legacy, target, 8 * 1024 * 1024, {
|
||||
...HEADER,
|
||||
unadaptedLegacyConfigCount: 1,
|
||||
});
|
||||
assert.equal(withConfigs.result.footer.outcome, 'manual_required');
|
||||
assert.equal(withConfigs.result.footer.unadaptedLegacyConfigCount, 1);
|
||||
});
|
||||
|
||||
test('fails closed before exceeding the plan byte budget', (t) => {
|
||||
const { legacy, target } = databases();
|
||||
t.after(() => legacy.close());
|
||||
|
||||
Reference in New Issue
Block a user