|
|
@@ -0,0 +1,115 @@
|
|
|
+// Local diagnostic: transpile the real cache service, using synthetic identity
|
|
|
+// and in-memory storage. No application bundle, server, network or real session.
|
|
|
+import assert from 'node:assert/strict';
|
|
|
+import { readFileSync } from 'node:fs';
|
|
|
+import vm from 'node:vm';
|
|
|
+import ts from 'typescript';
|
|
|
+import { firstValueFrom, of } from 'rxjs';
|
|
|
+import * as rxjs from 'rxjs';
|
|
|
+
|
|
|
+const servicePath = new URL('../../projects/xiaoshu-mobile/src/app/core/home-daily-cache.service.ts', import.meta.url);
|
|
|
+const compiled = ts.transpileModule(readFileSync(servicePath, 'utf8'), {
|
|
|
+ compilerOptions: {
|
|
|
+ target: ts.ScriptTarget.ES2022,
|
|
|
+ module: ts.ModuleKind.CommonJS,
|
|
|
+ experimentalDecorators: true,
|
|
|
+ },
|
|
|
+}).outputText;
|
|
|
+const STORAGE_KEY = 'xiaoshu.learning.homeDaily.v1';
|
|
|
+const morning = '2026-11-14T01:00:00.000Z';
|
|
|
+const afternoon = '2026-11-14T07:00:00.000Z';
|
|
|
+const identity = '20|1|2026-11-14';
|
|
|
+const oldPage = {
|
|
|
+ items: [], dates: [],
|
|
|
+ summary: { total: 0, completed: 0, due: 0, today: 0, income: 0 },
|
|
|
+ legacyReviewSync: { state: 'current' },
|
|
|
+};
|
|
|
+
|
|
|
+function fixture(value) {
|
|
|
+ const storage = new Map([[STORAGE_KEY, JSON.stringify({
|
|
|
+ identity, values: { review: { value, updatedAt: morning } },
|
|
|
+ })]]);
|
|
|
+ class LocalDate extends Date {
|
|
|
+ constructor(...args) { super(...(args.length ? args : [afternoon])); }
|
|
|
+ static now() { return Date.parse(afternoon); }
|
|
|
+ }
|
|
|
+ const session = {
|
|
|
+ user: () => ({ userId: 20, groupId: 1 }),
|
|
|
+ token: () => 'isolated-cache-audit-fixture',
|
|
|
+ };
|
|
|
+ const sandbox = {
|
|
|
+ exports: {}, Date: LocalDate,
|
|
|
+ localStorage: {
|
|
|
+ getItem: key => storage.get(key) ?? null,
|
|
|
+ setItem: (key, item) => storage.set(key, item),
|
|
|
+ removeItem: key => storage.delete(key),
|
|
|
+ },
|
|
|
+ require: name => {
|
|
|
+ if (name === '@angular/core') return {
|
|
|
+ Injectable: () => target => target,
|
|
|
+ inject: token => {
|
|
|
+ if (token === 'SessionService') return session;
|
|
|
+ if (token === 'NgZone') return {};
|
|
|
+ throw new Error(`Unexpected injection: ${token}`);
|
|
|
+ },
|
|
|
+ NgZone: 'NgZone',
|
|
|
+ };
|
|
|
+ if (name === 'rxjs') return rxjs;
|
|
|
+ if (name === './session.service') return { SessionService: 'SessionService' };
|
|
|
+ if (name === './teacher-today-review.service') return { beijingToday: () => '2026-11-14' };
|
|
|
+ throw new Error(`Unexpected module: ${name}`);
|
|
|
+ },
|
|
|
+ };
|
|
|
+ vm.runInNewContext(compiled, sandbox, { filename: servicePath.pathname });
|
|
|
+ return new sandbox.exports.HomeDailyCacheService();
|
|
|
+}
|
|
|
+
|
|
|
+const reports = [];
|
|
|
+{
|
|
|
+ const cache = fixture(oldPage);
|
|
|
+ let fetches = 0;
|
|
|
+ const snapshot = await firstValueFrom(cache.load('review', () => {
|
|
|
+ fetches++;
|
|
|
+ return of({ ...oldPage, view: 'home', legacyReviewSync: { state: 'snapshot' } });
|
|
|
+ }));
|
|
|
+ assert.equal(fetches, 0);
|
|
|
+ assert.equal(snapshot.value.legacyReviewSync.state, 'current');
|
|
|
+ assert.equal(snapshot.value.view, undefined);
|
|
|
+ reports.push({
|
|
|
+ case: 'same-day cache from old release bypasses new home endpoint',
|
|
|
+ fetches, retainedLegacyReviewSyncState: snapshot.value.legacyReviewSync.state,
|
|
|
+ hasNewHomeView: snapshot.value.view === 'home',
|
|
|
+ implication: 'HomePage reviewSyncNotice requires snapshot state, so this cached response does not show the new notice.',
|
|
|
+ });
|
|
|
+}
|
|
|
+{
|
|
|
+ const cache = fixture({
|
|
|
+ ...oldPage, view: 'home',
|
|
|
+ legacyReviewSync: { state: 'snapshot', lastSuccessAt: morning },
|
|
|
+ syncStatus: { state: 'current', lastSuccessAt: morning },
|
|
|
+ });
|
|
|
+ let fetches = 0;
|
|
|
+ const fetch = () => {
|
|
|
+ fetches++;
|
|
|
+ return of({ ...oldPage, view: 'home', legacyReviewSync: { state: 'snapshot' }, syncStatus: { state: 'delayed' } });
|
|
|
+ };
|
|
|
+ const snapshot = await firstValueFrom(cache.load('review', fetch));
|
|
|
+ assert.equal(fetches, 0);
|
|
|
+ assert.equal(snapshot.value.syncStatus.state, 'current');
|
|
|
+ assert.equal(snapshot.updatedAt, morning);
|
|
|
+ const forced = await firstValueFrom(cache.load('review', fetch, true));
|
|
|
+ assert.equal(fetches, 1);
|
|
|
+ assert.equal(forced.value.syncStatus.state, 'delayed');
|
|
|
+ assert.equal(forced.updatedAt, afternoon);
|
|
|
+ reports.push({
|
|
|
+ case: 'same-day cached sync status stays current until forced reload',
|
|
|
+ simulatedNow: afternoon, cachedAt: snapshot.updatedAt,
|
|
|
+ ordinaryFetches: 0, ordinaryState: snapshot.value.syncStatus.state,
|
|
|
+ forcedFetches: fetches, forcedState: forced.value.syncStatus.state,
|
|
|
+ implication: 'The stored sync status describes the morning read, not a live check at the later visit.',
|
|
|
+ });
|
|
|
+}
|
|
|
+console.log(JSON.stringify({
|
|
|
+ scope: 'Local service/cache contract only; no network, production identity, business writes or production-cause inference.',
|
|
|
+ passed: reports.length, reports,
|
|
|
+}, null, 2));
|