feat: dashboard source coverage를 안전하게 활성화
엔진의 부대·예약턴·집계 변화와 mailbox writer를 저널에 연결하고 dashboard.global 및 인증 아이콘 source를 dependency vector에 포함한다. post-deploy coverage v1 활성화는 shared head seed와 CAS를 한 transaction으로 수행한다.
This commit is contained in:
@@ -117,6 +117,7 @@ const installSourceRevisionState = (
|
||||
cityId: 0,
|
||||
nationId: 0,
|
||||
coverageVersion: 1,
|
||||
globalRevision: 1n,
|
||||
generalRevision: 1n,
|
||||
cityRevision: 0n,
|
||||
nationRevision: 0n,
|
||||
|
||||
@@ -8,6 +8,7 @@ const row = (overrides: Record<string, unknown> = {}) => ({
|
||||
cityId: 3,
|
||||
nationId: 2,
|
||||
coverageVersion: 1,
|
||||
globalRevision: 10n,
|
||||
generalRevision: 11n,
|
||||
cityRevision: 12n,
|
||||
nationRevision: 13n,
|
||||
@@ -16,9 +17,13 @@ const row = (overrides: Record<string, unknown> = {}) => ({
|
||||
...overrides,
|
||||
});
|
||||
|
||||
const read = async (value: unknown) => {
|
||||
const read = async (value: unknown, authSource?: Parameters<typeof readDashboardSourceRevisionState>[2]) => {
|
||||
const queryRaw = vi.fn(async (_query: unknown) => value);
|
||||
const state = await readDashboardSourceRevisionState({ $queryRaw: queryRaw } as Pick<DatabaseClient, '$queryRaw'>, 7);
|
||||
const state = await readDashboardSourceRevisionState(
|
||||
{ $queryRaw: queryRaw } as Pick<DatabaseClient, '$queryRaw'>,
|
||||
7,
|
||||
authSource
|
||||
);
|
||||
return { queryRaw, state };
|
||||
};
|
||||
|
||||
@@ -26,6 +31,7 @@ describe('dashboard source revision', () => {
|
||||
it('uses zero for missing revision rows and returns opaque 22-character hashes', async () => {
|
||||
const { queryRaw, state } = await read([
|
||||
row({
|
||||
globalRevision: 0n,
|
||||
generalRevision: 0n,
|
||||
cityRevision: 0n,
|
||||
nationRevision: 0n,
|
||||
@@ -41,19 +47,23 @@ describe('dashboard source revision', () => {
|
||||
expect.stringMatching(/^[A-Za-z0-9_-]{22}$/u),
|
||||
]);
|
||||
const statement = queryRaw.mock.calls[0]?.[0] as { sql: string };
|
||||
expect(statement.sql.match(/COALESCE\([^)]*\."revision", 0\)/gu)).toHaveLength(5);
|
||||
expect(statement.sql.match(/COALESCE\([^)]*\."revision", 0\)/gu)).toHaveLength(6);
|
||||
});
|
||||
|
||||
it('hashes exactly the documented context, command, and board dependency vectors', async () => {
|
||||
const initial = (await read([row()])).state;
|
||||
const globalChanged = (await read([row({ globalRevision: 99n })])).state;
|
||||
const cityChanged = (await read([row({ cityRevision: 99n })])).state;
|
||||
const accessChanged = (await read([row({ accessRevision: 99n })])).state;
|
||||
const worldChanged = (await read([row({ worldRevision: 99n })])).state;
|
||||
const nationChanged = (await read([row({ nationRevision: 99n })])).state;
|
||||
if (!initial || !cityChanged || !accessChanged || !worldChanged || !nationChanged) {
|
||||
if (!initial || !globalChanged || !cityChanged || !accessChanged || !worldChanged || !nationChanged) {
|
||||
throw new Error('source revision state missing');
|
||||
}
|
||||
|
||||
expect(globalChanged.sourceRevisions.context).not.toBe(initial.sourceRevisions.context);
|
||||
expect(globalChanged.sourceRevisions.commandTable).not.toBe(initial.sourceRevisions.commandTable);
|
||||
expect(globalChanged.sourceRevisions.boardAccess).toBe(initial.sourceRevisions.boardAccess);
|
||||
expect(cityChanged.sourceRevisions.context).not.toBe(initial.sourceRevisions.context);
|
||||
expect(cityChanged.sourceRevisions.commandTable).not.toBe(initial.sourceRevisions.commandTable);
|
||||
expect(cityChanged.sourceRevisions.boardAccess).toBe(initial.sourceRevisions.boardAccess);
|
||||
@@ -68,6 +78,40 @@ describe('dashboard source revision', () => {
|
||||
expect(nationChanged.sourceRevisions.boardAccess).not.toBe(initial.sourceRevisions.boardAccess);
|
||||
});
|
||||
|
||||
it('includes only the authenticated icon projection in the context source', async () => {
|
||||
const initial = (
|
||||
await read([row()], {
|
||||
canUseGeneralPicture: true,
|
||||
icons: [
|
||||
{
|
||||
id: 'icon-1',
|
||||
picture: 'icon-a.png',
|
||||
imageServer: 1,
|
||||
createdAt: '2026-08-16T00:00:00.000Z',
|
||||
},
|
||||
],
|
||||
})
|
||||
).state;
|
||||
const changed = (
|
||||
await read([row()], {
|
||||
canUseGeneralPicture: false,
|
||||
icons: [
|
||||
{
|
||||
id: 'icon-1',
|
||||
picture: 'icon-a.png',
|
||||
imageServer: 1,
|
||||
createdAt: '2026-08-16T00:00:00.000Z',
|
||||
},
|
||||
],
|
||||
})
|
||||
).state;
|
||||
if (!initial || !changed) throw new Error('source revision state missing');
|
||||
|
||||
expect(changed.sourceRevisions.context).not.toBe(initial.sourceRevisions.context);
|
||||
expect(changed.sourceRevisions.commandTable).toBe(initial.sourceRevisions.commandTable);
|
||||
expect(changed.sourceRevisions.boardAccess).toBe(initial.sourceRevisions.boardAccess);
|
||||
});
|
||||
|
||||
it('rejects missing meta/actor rows, malformed values, and query failures', async () => {
|
||||
await expect(read([])).resolves.toMatchObject({ state: null });
|
||||
await expect(read([row({ coverageVersion: -1 })])).resolves.toMatchObject({ state: null });
|
||||
|
||||
@@ -894,7 +894,10 @@ describe('appRouter', () => {
|
||||
actionCode: 'che_화계',
|
||||
arg: { destCityId: 7 },
|
||||
});
|
||||
expect(changeJournal.snapshot()).toEqual([{ domain: 'reserved.general', entityId: 13 }]);
|
||||
expect(changeJournal.snapshot()).toEqual([
|
||||
{ domain: 'dashboard.global', entityId: 0 },
|
||||
{ domain: 'reserved.general', entityId: 13 },
|
||||
]);
|
||||
|
||||
await expect(
|
||||
caller.turns.reserved.setGeneral({
|
||||
|
||||
Reference in New Issue
Block a user