@substrat-run/contract-tests 0.141.0 → 0.142.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/become-mint-suite.d.ts +12 -0
- package/dist/become-mint-suite.d.ts.map +1 -0
- package/dist/become-mint-suite.js +457 -0
- package/dist/become-mint-suite.js.map +1 -0
- package/dist/directory-restore-suite.js +1 -1
- package/dist/directory-restore-suite.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/modules.js +1 -1
- package/dist/modules.js.map +1 -1
- package/dist/permission-suite.js +2 -2
- package/dist/permission-suite.js.map +1 -1
- package/dist/scope-host-suite.d.ts.map +1 -1
- package/dist/scope-host-suite.js +334 -1
- package/dist/scope-host-suite.js.map +1 -1
- package/package.json +3 -3
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"scope-host-suite.d.ts","sourceRoot":"","sources":["../src/scope-host-suite.ts"],"names":[],"mappings":"AAEA,OAAO,EAqBL,KAAK,SAAS,EAkBf,MAAM,yBAAyB,CAAC;AACjC,OAAO,
|
|
1
|
+
{"version":3,"file":"scope-host-suite.d.ts","sourceRoot":"","sources":["../src/scope-host-suite.ts"],"names":[],"mappings":"AAEA,OAAO,EAqBL,KAAK,SAAS,EAkBf,MAAM,yBAAyB,CAAC;AACjC,OAAO,EAeL,KAAK,SAAS,EACf,MAAM,sBAAsB,CAAC;AAY9B,MAAM,WAAW,gBAAgB;IAC/B,IAAI,EAAE,SAAS,CAAC;IAChB;;;;;OAKG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CAC1B;AA2BD;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,eAAO,MAAM,aAAa,MACrB,OAAO,CAAC,OAAO,CAAC,QACb,SAAS,YACL,MAAM,KACf,OAAO,CAAC,IAAI,CAOd,CAAC;AAEF,iFAAiF;AACjF,MAAM,WAAW,qBAAqB;IACpC;;;;;;OAMG;IACH,2BAA2B,CAAC,EAAE,OAAO,CAAC;CACvC;AAED;;;;;GAKG;AACH,wBAAgB,sBAAsB,CACpC,WAAW,EAAE,MAAM,EACnB,WAAW,EAAE,MAAM,OAAO,CAAC,gBAAgB,CAAC,EAC5C,IAAI,GAAE,qBAA0B,GAC/B,IAAI,CAs+SN"}
|
package/dist/scope-host-suite.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
|
|
2
2
|
import { connectorCalls, connectorTestFetch, resetConnectorCalls } from './connector-fixture.js';
|
|
3
3
|
import { connectionId, dataSubjectId, domainEvent, errorCodeOf, moduleId, SCOPE_GATE_REASONS, eventId, instant, moduleManifest, orgId, AUTO_ADMISSION_NOTE, permissionKey, platformActorId, platformRequestId, principalId, scopeId, tenantId, SCOPE_QUERY_ROW_MAX, KERNEL_AUTHORED_EVENT_TYPES, EMAIL_SENT, PLATFORM_REQUEST_ENTITY_TYPE, SEND_EMAIL_KIND, SWEEP_RUNS_KIND, SCOPE_TABLE_PAGE_MAX, } from '@substrat-run/contracts';
|
|
4
|
-
import { INERT_SCOPE_REASON, SCOPE_COPY_LEASE_MS, isSearchIndexTable, platformIntentFailureMessage, REDACTED_DELIVERY_NOTE, REDACTED_FAILURE_NOTE, REDACTED_INTENT_MARKER, runPlatformSweep, startPlatformSweeper, createUlid, ulid, } from '@substrat-run/kernel';
|
|
4
|
+
import { INERT_SCOPE_REASON, BACKFILL_MOVE_ID, SCOPE_COPY_LEASE_MS, isSearchIndexTable, platformIntentFailureMessage, REDACTED_DELIVERY_NOTE, REDACTED_FAILURE_NOTE, REDACTED_INTENT_MARKER, runPlatformSweep, startPlatformSweeper, createUlid, ulid, } from '@substrat-run/kernel';
|
|
5
5
|
import { billedMod, contractTestBareOps, contractTestInitialModules, gateModManifest, lateMod, TEST_LIFECYCLE, testModManifest, victimModManifest, } from './modules.js';
|
|
6
6
|
/**
|
|
7
7
|
* Assert a refusal by the CODE it declared, not by the sentence it happens to carry
|
|
@@ -4930,6 +4930,48 @@ export function scopeHostContractSuite(adapterName, makeFixture, opts = {}) {
|
|
|
4930
4930
|
}
|
|
4931
4931
|
expect(beforeClaim.map((copy) => copy.moveId).sort()).toEqual([first, next].sort());
|
|
4932
4932
|
});
|
|
4933
|
+
it('a backfilled copy is retained, once per script, never over a ledgered one, and never under a reap claim (#1722)', async () => {
|
|
4934
|
+
const s = scopeId.parse(ulid());
|
|
4935
|
+
await host.provisionScope(staff, { tenantId: t1, scopeId: s });
|
|
4936
|
+
const [historic, tracked] = [`version-${ulid().toLowerCase()}`, `version-${ulid().toLowerCase()}`];
|
|
4937
|
+
const from = ulid();
|
|
4938
|
+
expect(await host.admin.backfillScopeScriptCopy(staff, t1, s, historic, { fromLogId: from })).toBe('recorded');
|
|
4939
|
+
expect(await host.admin.backfillScopeScriptCopy(staff, t1, s, historic)).toBe('ledgered'); // a re-run writes nothing
|
|
4940
|
+
// Audited once, naming the script and the log row it was read from.
|
|
4941
|
+
expect((await host.admin.auditLog(staff, { scopeId: s, action: 'backfillScopeCopy' })).map((r) => r.after))
|
|
4942
|
+
.toEqual([{ scriptRef: historic, fromLogId: from }]);
|
|
4943
|
+
const [entry] = await host.admin.listScopeScriptCopies(staff, { tenantId: t1, scopeId: s });
|
|
4944
|
+
expect(entry).toMatchObject({ scriptRef: historic, moveId: BACKFILL_MOVE_ID, state: 'retained', role: null, leaseUntil: null });
|
|
4945
|
+
// Never pending, so no lease sweep ever claims it, and a reap is not held off by it.
|
|
4946
|
+
expect((await host.admin.claimExpiredScopeScriptCopies(staff, {
|
|
4947
|
+
now: new Date(Date.now() + 365 * 86_400_000).toISOString(), leaseUntil: new Date().toISOString(), owner: 'sweep:test', limit: 1000,
|
|
4948
|
+
})).filter((c) => c.scopeId === s)).toEqual([]);
|
|
4949
|
+
// A script the ledger already names for this scope, in ANY state, is not backfilled again.
|
|
4950
|
+
const move = ulid();
|
|
4951
|
+
await host.admin.recordScopeScriptCopy(staff, t1, s, tracked, move, { role: 'source' });
|
|
4952
|
+
await host.admin.settleScopeScriptCopy(staff, t1, s, tracked, move, 'done');
|
|
4953
|
+
expect(await host.admin.backfillScopeScriptCopy(staff, t1, s, tracked)).toBe('ledgered');
|
|
4954
|
+
expect((await host.admin.listScopeScriptCopies(staff, { tenantId: t1, scopeId: s })).map((c) => c.moveId).sort())
|
|
4955
|
+
.toEqual([BACKFILL_MOVE_ID, move].sort());
|
|
4956
|
+
// A move that later carries the store out of the historic script takes it over: while its
|
|
4957
|
+
// own entry is live the backfilled one stays, and once that entry is done (wiped, or the
|
|
4958
|
+
// route) the backfilled one settles with it, so no reap or erasure is sent there again.
|
|
4959
|
+
const carry = ulid();
|
|
4960
|
+
await host.admin.recordScopeScriptCopy(staff, t1, s, historic, carry, { role: 'source' });
|
|
4961
|
+
await host.admin.settleScopeScriptCopy(staff, t1, s, historic, carry, 'eligible', { loadStamp: 'st', revision: '1' });
|
|
4962
|
+
expect((await host.admin.listScopeScriptCopies(staff, { tenantId: t1, scopeId: s, state: 'retained' })).map((c) => c.scriptRef))
|
|
4963
|
+
.toEqual([historic]);
|
|
4964
|
+
await host.admin.settleScopeScriptCopy(staff, t1, s, historic, carry, 'done');
|
|
4965
|
+
expect(await host.admin.listScopeScriptCopies(staff, { tenantId: t1, scopeId: s, state: 'retained' })).toEqual([]);
|
|
4966
|
+
// Scoped by tenant: the same scope id under another tenant is unknown.
|
|
4967
|
+
expect(await host.admin.backfillScopeScriptCopy(staff, t2, s, `version-${ulid().toLowerCase()}`)).toBe('missing');
|
|
4968
|
+
expect(await host.admin.backfillScopeScriptCopy(staff, t1, scopeId.parse(ulid()), historic)).toBe('missing');
|
|
4969
|
+
// Under a reap claim nothing new enters the ledger, so the reap's own listing stays whole.
|
|
4970
|
+
await host.admin.beginScopeScriptReap(staff, t1, s);
|
|
4971
|
+
const late = `version-${ulid().toLowerCase()}`;
|
|
4972
|
+
expect(await host.admin.backfillScopeScriptCopy(staff, t1, s, late)).toBe('reaping');
|
|
4973
|
+
expect((await host.admin.listScopeScriptCopies(staff, { tenantId: t1, scopeId: s })).some((c) => c.scriptRef === late)).toBe(false);
|
|
4974
|
+
});
|
|
4933
4975
|
it('a pending copy entry is claimed only once its lease ran out, and only once (#1722)', async () => {
|
|
4934
4976
|
const s = scopeId.parse(ulid());
|
|
4935
4977
|
await host.provisionScope(staff, { tenantId: t1, scopeId: s });
|
|
@@ -4992,6 +5034,38 @@ export function scopeHostContractSuite(adapterName, makeFixture, opts = {}) {
|
|
|
4992
5034
|
// And it confirms once: the same confirmation again refuses.
|
|
4993
5035
|
await expectRefusal(host.admin.setScopeServingRef(staff, t1, s, from, { expectedErasureEpoch: 0, confirmMove: { moveId: move, source: 'eligible' } }), 'precondition_failed');
|
|
4994
5036
|
});
|
|
5037
|
+
it('a backfill and every erasure record the erasure epoch they ran under (#1722)', async () => {
|
|
5038
|
+
const s = scopeId.parse(ulid());
|
|
5039
|
+
await host.provisionScope(staff, { tenantId: t1, scopeId: s });
|
|
5040
|
+
const epochs = async (action) => (await host.admin.auditLog(staff, { scopeId: s, action })).map((r) => r.before);
|
|
5041
|
+
expect(await host.admin.backfillScopeScriptCopy(staff, t1, s, `version-${ulid().toLowerCase()}`)).toBe('recorded');
|
|
5042
|
+
const redacted = [{ events: 0, intents: 0, jobRuns: 0, idempotencyResults: 0, intentIds: [],
|
|
5043
|
+
vertical: { verticalRows: [], hookRows: [], unreachedEntities: [] } }];
|
|
5044
|
+
await host.admin.finalizeSubjectShred(staff, t1, s, ulid(), redacted, { versionId: null, servingRef: null, epoch: 0, copyCount: 1 });
|
|
5045
|
+
expect(await host.admin.backfillScopeScriptCopy(staff, t1, s, `version-${ulid().toLowerCase()}`)).toBe('recorded');
|
|
5046
|
+
expect(await epochs('backfillScopeCopy')).toEqual([{ erasureEpoch: 0 }, { erasureEpoch: 1 }]);
|
|
5047
|
+
expect(await epochs('shredSubject')).toEqual([{ erasureEpoch: 0, path: 'orchestrated' }]);
|
|
5048
|
+
// A direct shred claims nothing, and says so, with the epoch it ran under.
|
|
5049
|
+
await host.admin.shredSubject(staff, t1, s, ulid());
|
|
5050
|
+
expect(await epochs('shredSubject')).toEqual([{ erasureEpoch: 0, path: 'orchestrated' }, { erasureEpoch: 1, path: 'direct' }]);
|
|
5051
|
+
});
|
|
5052
|
+
it('a move that routes onto a backfilled script settles that entry in the confirming write (#1722)', async () => {
|
|
5053
|
+
const s = scopeId.parse(ulid());
|
|
5054
|
+
await host.provisionScope(staff, { tenantId: t1, scopeId: s });
|
|
5055
|
+
const [from, to, other] = [`version-${ulid().toLowerCase()}`, `version-${ulid().toLowerCase()}`, `version-${ulid().toLowerCase()}`];
|
|
5056
|
+
expect(await host.admin.backfillScopeScriptCopy(staff, t1, s, to)).toBe('recorded');
|
|
5057
|
+
expect(await host.admin.backfillScopeScriptCopy(staff, t1, s, other)).toBe('recorded');
|
|
5058
|
+
const move = ulid();
|
|
5059
|
+
await host.admin.recordScopeScriptCopy(staff, t1, s, from, move, { role: 'source' });
|
|
5060
|
+
await host.admin.recordScopeScriptCopy(staff, t1, s, to, move, { role: 'destination', loadStamp: 'restore-stamp' });
|
|
5061
|
+
// Until it confirms, the move's own entry is pending: the backfilled one still stands.
|
|
5062
|
+
expect((await host.admin.listScopeScriptCopies(staff, { tenantId: t1, scopeId: s, state: 'retained' })).map((c) => c.scriptRef).sort())
|
|
5063
|
+
.toEqual([other, to].sort());
|
|
5064
|
+
await host.admin.setScopeServingRef(staff, t1, s, to, { expectedErasureEpoch: 0, confirmMove: { moveId: move, source: 'retained' } });
|
|
5065
|
+
const byRef = Object.fromEntries((await host.admin.listScopeScriptCopies(staff, { tenantId: t1, scopeId: s }))
|
|
5066
|
+
.filter((c) => c.moveId === BACKFILL_MOVE_ID).map((c) => [c.scriptRef, c.state]));
|
|
5067
|
+
expect(byRef).toEqual({ [to]: 'done', [other]: 'retained' });
|
|
5068
|
+
});
|
|
4995
5069
|
it('a reap claim refuses a conditional bind until the scope row is deleted (#1722)', async () => {
|
|
4996
5070
|
const s = scopeId.parse(ulid());
|
|
4997
5071
|
await host.provisionScope(staff, { tenantId: t1, scopeId: s });
|
|
@@ -7664,6 +7738,265 @@ export function scopeHostContractSuite(adapterName, makeFixture, opts = {}) {
|
|
|
7664
7738
|
await host.admin.revokeEntitlement(staff, mt, 'billed');
|
|
7665
7739
|
await host.admin.revokeEntitlement(staff, mt, 'aux-sku');
|
|
7666
7740
|
});
|
|
7741
|
+
// -- the stored storage gauge (#1524) --------------------------------------
|
|
7742
|
+
describe('the stored storage gauge (#1524)', () => {
|
|
7743
|
+
const day = (offsetDays, time) => `${new Date(Date.now() + offsetDays * 86_400_000).toISOString().slice(0, 10)}T${time}.000Z`;
|
|
7744
|
+
const newScope = async (tenant) => {
|
|
7745
|
+
const s = scopeId.parse(ulid());
|
|
7746
|
+
await host.provisionScope(staff, { tenantId: tenant, scopeId: s });
|
|
7747
|
+
await host.admin.activateScope(staff, tenant, s);
|
|
7748
|
+
return s;
|
|
7749
|
+
};
|
|
7750
|
+
const record = (tenant, scope, bytes, readAt) => host.admin.recordScopeStorage(staff, [{ tenantId: tenant, scopeId: scope, bytes, readAt }]);
|
|
7751
|
+
it('keeps one row per scope per UTC day: a same-day reading replaces an older one, the next day adds a row', async () => {
|
|
7752
|
+
const tg = tenantId.parse(ulid());
|
|
7753
|
+
await host.admin.createTenant(staff, { id: tg, slug: 'gauge-days', name: 'Gauge Days' });
|
|
7754
|
+
const s = await newScope(tg);
|
|
7755
|
+
expect(await record(tg, s, 100, day(-1, '10:00:00'))).toEqual({ recorded: 1 });
|
|
7756
|
+
expect(await record(tg, s, 200, day(-1, '11:00:00'))).toEqual({ recorded: 1 });
|
|
7757
|
+
// An OLDER reading arriving late never overwrites a newer one of the same day.
|
|
7758
|
+
expect(await record(tg, s, 50, day(-1, '09:00:00'))).toEqual({ recorded: 0 });
|
|
7759
|
+
expect(await record(tg, s, 300, day(0, '00:00:00'))).toEqual({ recorded: 1 });
|
|
7760
|
+
const history = await host.admin.listScopeStorage(staff, { scopeId: s });
|
|
7761
|
+
expect(history.map((r) => [r.day, r.bytes])).toEqual([
|
|
7762
|
+
[day(-1, '00:00:00').slice(0, 10), 200],
|
|
7763
|
+
[day(0, '00:00:00').slice(0, 10), 300],
|
|
7764
|
+
]);
|
|
7765
|
+
expect(await host.admin.listScopeStorage(staff, { scopeId: s, latest: true })).toEqual([
|
|
7766
|
+
{ tenantId: tg, scopeId: s, day: day(0, '00:00:00').slice(0, 10), bytes: 300, readAt: day(0, '00:00:00') },
|
|
7767
|
+
]);
|
|
7768
|
+
// History is bounded and windowed by day.
|
|
7769
|
+
expect(await host.admin.listScopeStorage(staff, { scopeId: s, limit: 1 })).toHaveLength(1);
|
|
7770
|
+
expect((await host.admin.listScopeStorage(staff, { scopeId: s, since: day(0, '00:00:00').slice(0, 10) })).map((r) => r.bytes)).toEqual([300]);
|
|
7771
|
+
// K-24: the read is attributable.
|
|
7772
|
+
const reads = await host.admin.accessLog(staff, { method: 'listScopeStorage' });
|
|
7773
|
+
expect(reads.some((r) => r.scopeId === s)).toBe(true);
|
|
7774
|
+
});
|
|
7775
|
+
it('records every attempt; a failed one keeps the last sample, and the attempt is what the pass reads next', async () => {
|
|
7776
|
+
const tf = tenantId.parse(ulid());
|
|
7777
|
+
await host.admin.createTenant(staff, { id: tf, slug: 'gauge-tries', name: 'Gauge Tries' });
|
|
7778
|
+
const [s, declined] = [await newScope(tf), await newScope(tf)];
|
|
7779
|
+
await record(tf, s, 700, day(-1, '05:00:00'));
|
|
7780
|
+
expect(await host.admin.recordScopeStorage(staff, [
|
|
7781
|
+
{ tenantId: tf, scopeId: s, bytes: null, error: 'vertical answered 501', readAt: day(0, '05:00:00') },
|
|
7782
|
+
{ tenantId: tf, scopeId: declined, bytes: null, readAt: day(0, '05:00:00') },
|
|
7783
|
+
// Under the wrong tenant: no attempt row either.
|
|
7784
|
+
{ tenantId: tenantId.parse(ulid()), scopeId: s, bytes: null, error: 'x', readAt: day(0, '06:00:00') },
|
|
7785
|
+
])).toEqual({ recorded: 0 });
|
|
7786
|
+
// The last good sample stands.
|
|
7787
|
+
expect((await host.admin.listScopeStorage(staff, { scopeId: s, latest: true })).map((r) => r.bytes)).toEqual([700]);
|
|
7788
|
+
const attempts = (await host.admin.listScopeStorageAttempts(staff)).filter((a) => a.tenantId === tf);
|
|
7789
|
+
expect(attempts.map((a) => [a.scopeId, a.attemptedAt, a.error]).sort()).toEqual([
|
|
7790
|
+
[s, day(0, '05:00:00'), 'vertical answered 501'],
|
|
7791
|
+
[declined, day(0, '05:00:00'), null],
|
|
7792
|
+
].sort());
|
|
7793
|
+
// /meters names the failing scope rather than letting it go missing; its last good
|
|
7794
|
+
// reading is still in the sum. A declined scope is not failing.
|
|
7795
|
+
expect((await host.admin.readMeters(staff, { tenantId: tf })).perTenant[0].storage).toMatchObject({
|
|
7796
|
+
bytes: 700,
|
|
7797
|
+
sampled: 1,
|
|
7798
|
+
total: 2,
|
|
7799
|
+
failing: 1,
|
|
7800
|
+
lastFailedAt: day(0, '05:00:00'),
|
|
7801
|
+
});
|
|
7802
|
+
// A successful read clears the error and moves the attempt on.
|
|
7803
|
+
await record(tf, s, 900, day(0, '07:00:00'));
|
|
7804
|
+
expect((await host.admin.readMeters(staff, { tenantId: tf })).perTenant[0].storage).toMatchObject({
|
|
7805
|
+
bytes: 900,
|
|
7806
|
+
failing: 0,
|
|
7807
|
+
lastFailedAt: null,
|
|
7808
|
+
});
|
|
7809
|
+
expect((await host.admin.listScopeStorageAttempts(staff)).find((a) => a.scopeId === s)).toMatchObject({
|
|
7810
|
+
attemptedAt: day(0, '07:00:00'),
|
|
7811
|
+
error: null,
|
|
7812
|
+
});
|
|
7813
|
+
});
|
|
7814
|
+
it("writes a reading only under the scope's own tenant, and folds each tenant's figure from its own scopes", async () => {
|
|
7815
|
+
const [ta, tb] = [tenantId.parse(ulid()), tenantId.parse(ulid())];
|
|
7816
|
+
await host.admin.createTenant(staff, { id: ta, slug: 'gauge-a', name: 'Gauge A' });
|
|
7817
|
+
await host.admin.createTenant(staff, { id: tb, slug: 'gauge-b', name: 'Gauge B' });
|
|
7818
|
+
const [a1, a2, b1] = [await newScope(ta), await newScope(ta), await newScope(tb)];
|
|
7819
|
+
// Named under the wrong tenant: refused by the directory, not merely filtered on read.
|
|
7820
|
+
expect(await record(tb, a1, 1_000_000, day(0, '01:00:00'))).toEqual({ recorded: 0 });
|
|
7821
|
+
expect(await host.admin.listScopeStorage(staff, { tenantId: tb })).toEqual([]);
|
|
7822
|
+
await host.admin.recordScopeStorage(staff, [
|
|
7823
|
+
{ tenantId: ta, scopeId: a1, bytes: 4096, readAt: day(-1, '06:00:00') },
|
|
7824
|
+
{ tenantId: ta, scopeId: a2, bytes: 8192, readAt: day(0, '06:00:00') },
|
|
7825
|
+
{ tenantId: tb, scopeId: b1, bytes: 10, readAt: day(0, '07:00:00') },
|
|
7826
|
+
]);
|
|
7827
|
+
const a = await host.admin.readMeters(staff, { tenantId: ta });
|
|
7828
|
+
expect(a.perTenant[0].storage).toEqual({
|
|
7829
|
+
basis: 'scope-databases',
|
|
7830
|
+
excluded: ['attachments', 'tenant-stores', 'lake'],
|
|
7831
|
+
bytes: 4096 + 8192,
|
|
7832
|
+
sampled: 2,
|
|
7833
|
+
total: 2,
|
|
7834
|
+
oldestReadAt: day(-1, '06:00:00'),
|
|
7835
|
+
newestReadAt: day(0, '06:00:00'),
|
|
7836
|
+
failing: 0,
|
|
7837
|
+
lastFailedAt: null,
|
|
7838
|
+
});
|
|
7839
|
+
expect(a.storage).toEqual(a.perTenant[0].storage);
|
|
7840
|
+
const b = await host.admin.readMeters(staff, { tenantId: tb });
|
|
7841
|
+
expect(b.perTenant[0].storage).toMatchObject({ bytes: 10, sampled: 1, total: 1 });
|
|
7842
|
+
// A new scope with no reading yet: the figure says so rather than calling itself whole.
|
|
7843
|
+
await newScope(tb);
|
|
7844
|
+
expect((await host.admin.readMeters(staff, { tenantId: tb })).perTenant[0].storage).toMatchObject({
|
|
7845
|
+
bytes: 10,
|
|
7846
|
+
sampled: 1,
|
|
7847
|
+
total: 2,
|
|
7848
|
+
});
|
|
7849
|
+
// The fleet figure is the per-tenant figures summed.
|
|
7850
|
+
const fleet = await host.admin.readMeters(staff);
|
|
7851
|
+
expect(fleet.storage.bytes).toBe(fleet.perTenant.reduce((n, r) => n + (r.storage?.bytes ?? 0), 0));
|
|
7852
|
+
expect(fleet.storage.sampled).toBe(fleet.perTenant.reduce((n, r) => n + (r.storage?.sampled ?? 0), 0));
|
|
7853
|
+
});
|
|
7854
|
+
it("forgets a reaped scope's samples, and never writes one for it again", async () => {
|
|
7855
|
+
const tr = tenantId.parse(ulid());
|
|
7856
|
+
await host.admin.createTenant(staff, { id: tr, slug: 'gauge-reap', name: 'Gauge Reap' });
|
|
7857
|
+
const [kept, reaped] = [await newScope(tr), await newScope(tr)];
|
|
7858
|
+
await record(tr, kept, 1, day(0, '02:00:00'));
|
|
7859
|
+
await record(tr, reaped, 2, day(-1, '02:00:00'));
|
|
7860
|
+
await record(tr, reaped, 3, day(0, '02:00:00'));
|
|
7861
|
+
// A sample past retention, so the prune below (which reads the raw table, with no join
|
|
7862
|
+
// to scope status) can tell whether reap deleted the rows or only the join hides them.
|
|
7863
|
+
while ((await host.admin.pruneScopeStorage(staff, 500)) > 0)
|
|
7864
|
+
;
|
|
7865
|
+
const old = new Date();
|
|
7866
|
+
old.setUTCMonth(old.getUTCMonth() - 14);
|
|
7867
|
+
expect(await record(tr, reaped, 5, old.toISOString())).toEqual({ recorded: 1 });
|
|
7868
|
+
await host.admin.archiveScope(staff, tr, reaped);
|
|
7869
|
+
await host.admin.reapScope(staff, tr, reaped);
|
|
7870
|
+
expect(await host.admin.pruneScopeStorage(staff, 500)).toBe(0);
|
|
7871
|
+
expect((await host.admin.listScopeStorage(staff, { tenantId: tr })).map((r) => r.scopeId)).toEqual([kept]);
|
|
7872
|
+
expect(await record(tr, reaped, 4, day(0, '03:00:00'))).toEqual({ recorded: 0 });
|
|
7873
|
+
expect((await host.admin.readMeters(staff, { tenantId: tr })).perTenant[0].storage).toMatchObject({
|
|
7874
|
+
bytes: 1,
|
|
7875
|
+
sampled: 1,
|
|
7876
|
+
total: 1, // the tombstone is not in the denominator
|
|
7877
|
+
});
|
|
7878
|
+
});
|
|
7879
|
+
it('keeps an archived scope in the figure and refreshes it, so the tenant is not stuck stale', async () => {
|
|
7880
|
+
const ta = tenantId.parse(ulid());
|
|
7881
|
+
await host.admin.createTenant(staff, { id: ta, slug: 'gauge-archived', name: 'Gauge Archived' });
|
|
7882
|
+
const s = await newScope(ta);
|
|
7883
|
+
const threeDaysAgo = new Date(Date.now() - 3 * 86_400_000).toISOString();
|
|
7884
|
+
await record(ta, s, 4096, threeDaysAgo);
|
|
7885
|
+
await host.admin.archiveScope(staff, ta, s);
|
|
7886
|
+
// Archived storage is still storage until reap: counted, and stale until re-read.
|
|
7887
|
+
expect((await host.admin.readMeters(staff, { tenantId: ta })).perTenant[0].storage).toMatchObject({
|
|
7888
|
+
sampled: 1,
|
|
7889
|
+
total: 1,
|
|
7890
|
+
oldestReadAt: threeDaysAgo,
|
|
7891
|
+
});
|
|
7892
|
+
// No drain reaches an archived scope; the storage phase reads it anyway, once a day.
|
|
7893
|
+
const before = new Date().toISOString();
|
|
7894
|
+
const report = await runPlatformSweep(host, {
|
|
7895
|
+
actor: staff,
|
|
7896
|
+
fetch: connectorTestFetch,
|
|
7897
|
+
sweepers: {},
|
|
7898
|
+
drainRetries: false,
|
|
7899
|
+
storageGauge: { read: (scope) => host.admin.scopeDatabaseSize(staff, scope.tenantId, scope.id), batch: 10_000 },
|
|
7900
|
+
});
|
|
7901
|
+
expect(report.errors.filter((e) => e.kind === 'storage' && e.id === s)).toEqual([]);
|
|
7902
|
+
const after = (await host.admin.readMeters(staff, { tenantId: ta })).perTenant[0].storage;
|
|
7903
|
+
expect(after).toMatchObject({ sampled: 1, total: 1, failing: 0 });
|
|
7904
|
+
expect(after.oldestReadAt >= before).toBe(true);
|
|
7905
|
+
expect(after.bytes).toBeGreaterThan(0);
|
|
7906
|
+
});
|
|
7907
|
+
it('leaves a provisioning scope, which has no store yet, out of the figure entirely', async () => {
|
|
7908
|
+
const tp = tenantId.parse(ulid());
|
|
7909
|
+
await host.admin.createTenant(staff, { id: tp, slug: 'gauge-provisioning', name: 'Gauge Provisioning' });
|
|
7910
|
+
const s = await newScope(tp);
|
|
7911
|
+
const pending = scopeId.parse(ulid());
|
|
7912
|
+
await host.provisionScope(staff, { tenantId: tp, scopeId: pending }); // not activated
|
|
7913
|
+
expect((await host.admin.getScopeRecord(staff, tp, pending)).status).toBe('provisioning');
|
|
7914
|
+
await record(tp, s, 10, day(0, '08:00:00'));
|
|
7915
|
+
// A reading for it is refused too, so it can never enter the sum.
|
|
7916
|
+
expect(await record(tp, pending, 99, day(0, '08:00:00'))).toEqual({ recorded: 0 });
|
|
7917
|
+
expect((await host.admin.readMeters(staff, { tenantId: tp })).perTenant[0].storage).toMatchObject({
|
|
7918
|
+
bytes: 10,
|
|
7919
|
+
sampled: 1,
|
|
7920
|
+
total: 1, // not "1 of 2": the tenant's figure is whole
|
|
7921
|
+
});
|
|
7922
|
+
});
|
|
7923
|
+
it('neither counts nor accepts a reading for a scope archived before it held data; one archived from active still counts', async () => {
|
|
7924
|
+
const tv = tenantId.parse(ulid());
|
|
7925
|
+
await host.admin.createTenant(staff, { id: tv, slug: 'gauge-abandoned', name: 'Gauge Abandoned' });
|
|
7926
|
+
const kept = await newScope(tv);
|
|
7927
|
+
await record(tv, kept, 50, day(0, '09:00:00'));
|
|
7928
|
+
// A failed provision is archived straight from `provisioning` (it never held data).
|
|
7929
|
+
const abandoned = scopeId.parse(ulid());
|
|
7930
|
+
await host.provisionScope(staff, { tenantId: tv, scopeId: abandoned });
|
|
7931
|
+
await host.admin.archiveScope(staff, tv, abandoned);
|
|
7932
|
+
expect((await host.admin.getScopeRecord(staff, tv, abandoned)).archivedFromStatus).toBe('provisioning');
|
|
7933
|
+
// An app deleted after it served is archived from `active`.
|
|
7934
|
+
const deleted = await newScope(tv);
|
|
7935
|
+
await record(tv, deleted, 70, day(-2, '09:00:00')); // due again by the time the pass runs
|
|
7936
|
+
await host.admin.archiveScope(staff, tv, deleted);
|
|
7937
|
+
expect((await host.admin.getScopeRecord(staff, tv, deleted)).archivedFromStatus).toBe('active');
|
|
7938
|
+
expect(await record(tv, abandoned, 99, day(0, '10:00:00'))).toEqual({ recorded: 0 });
|
|
7939
|
+
expect((await host.admin.readMeters(staff, { tenantId: tv })).perTenant[0].storage).toMatchObject({
|
|
7940
|
+
bytes: 120,
|
|
7941
|
+
sampled: 2,
|
|
7942
|
+
total: 2, // kept + deleted; never the abandoned one
|
|
7943
|
+
});
|
|
7944
|
+
// The storage phase never wakes it.
|
|
7945
|
+
const asked = [];
|
|
7946
|
+
await runPlatformSweep(host, {
|
|
7947
|
+
actor: staff,
|
|
7948
|
+
fetch: connectorTestFetch,
|
|
7949
|
+
sweepers: {},
|
|
7950
|
+
drainRetries: false,
|
|
7951
|
+
storageGauge: {
|
|
7952
|
+
read: async (scope) => {
|
|
7953
|
+
asked.push(scope.id);
|
|
7954
|
+
return host.admin.scopeDatabaseSize(staff, scope.tenantId, scope.id);
|
|
7955
|
+
},
|
|
7956
|
+
batch: 10_000,
|
|
7957
|
+
},
|
|
7958
|
+
});
|
|
7959
|
+
expect(asked).not.toContain(abandoned);
|
|
7960
|
+
expect(asked).toContain(deleted);
|
|
7961
|
+
// Unarchiving clears it, as it clears archivedAt.
|
|
7962
|
+
await host.admin.unarchiveScope(staff, tv, deleted);
|
|
7963
|
+
expect((await host.admin.getScopeRecord(staff, tv, deleted)).archivedFromStatus ?? null).toBeNull();
|
|
7964
|
+
});
|
|
7965
|
+
it("deletes a fork's samples when the fork is deleted", async () => {
|
|
7966
|
+
const tk = tenantId.parse(ulid());
|
|
7967
|
+
await host.admin.createTenant(staff, { id: tk, slug: 'gauge-fork', name: 'Gauge Fork' });
|
|
7968
|
+
const source = await newScope(tk);
|
|
7969
|
+
const fork = await host.snapshotScope(staff, tk, source);
|
|
7970
|
+
// A sample past retention: the prune reads the raw table, with no join to the scope
|
|
7971
|
+
// row, so it sees a leftover that every other read would hide once the row is gone.
|
|
7972
|
+
while ((await host.admin.pruneScopeStorage(staff, 500)) > 0)
|
|
7973
|
+
;
|
|
7974
|
+
const old = new Date();
|
|
7975
|
+
old.setUTCMonth(old.getUTCMonth() - 14);
|
|
7976
|
+
expect(await record(tk, fork, 9, old.toISOString())).toEqual({ recorded: 1 });
|
|
7977
|
+
await host.deleteSnapshot(staff, tk, fork);
|
|
7978
|
+
expect(await host.admin.pruneScopeStorage(staff, 500)).toBe(0);
|
|
7979
|
+
});
|
|
7980
|
+
it('prunes samples older than thirteen months, at most the limit per call', async () => {
|
|
7981
|
+
const tp = tenantId.parse(ulid());
|
|
7982
|
+
await host.admin.createTenant(staff, { id: tp, slug: 'gauge-prune', name: 'Gauge Prune' });
|
|
7983
|
+
const s = await newScope(tp);
|
|
7984
|
+
const monthsAgo = (m) => {
|
|
7985
|
+
const d = new Date();
|
|
7986
|
+
d.setUTCMonth(d.getUTCMonth() - m);
|
|
7987
|
+
return d.toISOString();
|
|
7988
|
+
};
|
|
7989
|
+
for (const m of [14, 15, 16])
|
|
7990
|
+
await record(tp, s, m, monthsAgo(m));
|
|
7991
|
+
await record(tp, s, 12, monthsAgo(12)); // inside retention: kept
|
|
7992
|
+
await record(tp, s, 0, day(0, '04:00:00'));
|
|
7993
|
+
expect(await host.admin.pruneScopeStorage(staff, 2)).toBe(2);
|
|
7994
|
+
expect(await host.admin.pruneScopeStorage(staff, 2)).toBe(1);
|
|
7995
|
+
expect(await host.admin.pruneScopeStorage(staff, 2)).toBe(0);
|
|
7996
|
+
expect((await host.admin.listScopeStorage(staff, { scopeId: s })).map((r) => r.bytes)).toEqual([12, 0]);
|
|
7997
|
+
await expect(host.admin.pruneScopeStorage(staff, -1)).rejects.toThrow(/limit/);
|
|
7998
|
+
});
|
|
7999
|
+
});
|
|
7667
8000
|
it('leaves bare (module-less) operations ungated', async () => {
|
|
7668
8001
|
// test/read-counter was registered via defineOperation, no manifest — it
|
|
7669
8002
|
// must resolve regardless of entitlements.
|