|
| 1 | +import assert from 'node:assert/strict'; |
| 2 | +import { test } from 'node:test'; |
| 3 | +import { |
| 4 | + findSessionStateWrites, |
| 5 | + SESSION_STATE_FIELD_OWNERS, |
| 6 | + sessionStateWritePressure, |
| 7 | +} from './session-state.ts'; |
| 8 | + |
| 9 | +const OWNER = 'src/daemon/app-log-session-resource.ts'; |
| 10 | +const FIELDS = ['appLog', 'appLogFailure', 'lease', 'lastPerfProfile']; |
| 11 | +function scan(source: string, file = OWNER) { |
| 12 | + return findSessionStateWrites(new Map([[file, source]]), FIELDS); |
| 13 | +} |
| 14 | + |
| 15 | +test('explicit owner patches name their writes and retain direct assignment checks', () => { |
| 16 | + assert.deepEqual( |
| 17 | + scan('store.update(ref, { appLogFailure: undefined });\nsession.appLog = log;').map((w) => [ |
| 18 | + w.field, |
| 19 | + w.line, |
| 20 | + ]), |
| 21 | + [ |
| 22 | + ['appLogFailure', 1], |
| 23 | + ['appLog', 2], |
| 24 | + ], |
| 25 | + ); |
| 26 | + assert.ok(SESSION_STATE_FIELD_OWNERS.appLogFailure!.includes(OWNER)); |
| 27 | + const foreign = scan( |
| 28 | + 'sessionStore.update(ref, { appLogFailure: error });', |
| 29 | + 'src/daemon/handlers/probe.ts', |
| 30 | + )[0]!; |
| 31 | + assert.equal(foreign.field, 'appLogFailure'); |
| 32 | + assert.ok(!SESSION_STATE_FIELD_OWNERS[foreign.field]!.includes(foreign.file)); |
| 33 | +}); |
| 34 | + |
| 35 | +test('inline synchronous patches can derive named fields with nested value spreads', () => { |
| 36 | + for (const patch of [ |
| 37 | + '(current) => ({ lease: { ...current.lease, expiresAt: 10 } })', |
| 38 | + '(current) => { const expiresAt = 10; return { lease: { ...current.lease, expiresAt } }; }', |
| 39 | + 'function(current) { return { lease: { ...current.lease, expiresAt: 10 } }; }', |
| 40 | + ]) |
| 41 | + assert.deepEqual( |
| 42 | + scan(`store.update(ref, ${patch});`).map((w) => w.field), |
| 43 | + ['lease'], |
| 44 | + patch, |
| 45 | + ); |
| 46 | +}); |
| 47 | + |
| 48 | +for (const patch of [ |
| 49 | + '{ [key]: value }', |
| 50 | + '{ ...changes }', |
| 51 | + 'changes', |
| 52 | + 'rebuild', |
| 53 | + 'async (current) => ({ appLogFailure: undefined })', |
| 54 | + '(current) => changes', |
| 55 | + '(current) => { if (test) return changes; return { appLogFailure: undefined }; }', |
| 56 | + '{ get appLogFailure() { return error; } }', |
| 57 | +]) { |
| 58 | + test(`update rejects unattributable patch ${patch}`, () => { |
| 59 | + assert.ok(scan(`store.update(ref, ${patch});`).some((w) => w.field === '[patch-shape]')); |
| 60 | + }); |
| 61 | +} |
| 62 | + |
| 63 | +test('patch callbacks cannot call back into the session store', () => { |
| 64 | + const writes = scan( |
| 65 | + 'store.update(ref, (current) => { store.retire(ref); return { appLogFailure: undefined }; });', |
| 66 | + ); |
| 67 | + assert.ok(writes.some((w) => w.field === '[reentrant-patch]')); |
| 68 | + assert.ok(writes.some((w) => w.field === 'appLogFailure')); |
| 69 | +}); |
| 70 | + |
| 71 | +test('the historical app-log whole-record spread is refused and counted fairly', () => { |
| 72 | + const writes = scan( |
| 73 | + 'const session = sessionStore.get(address); sessionStore.set(address, { ...session, appLogFailure: error });', |
| 74 | + ); |
| 75 | + assert.deepEqual( |
| 76 | + writes.map((w) => w.field), |
| 77 | + ['[whole-record-spread]', 'appLogFailure'], |
| 78 | + ); |
| 79 | +}); |
| 80 | + |
| 81 | +test('record copies through a read alias or captured ref remain visible', () => { |
| 82 | + for (const source of [ |
| 83 | + 'const refreshed = params.sessionStore.get(address) ?? session; return { ...refreshed, lastPerfProfile: profile };', |
| 84 | + 'const previous: SessionState = value; return { ...previous, lastPerfProfile: profile };', |
| 85 | + 'return { ...ref.session, lastPerfProfile: profile };', |
| 86 | + ]) |
| 87 | + assert.deepEqual( |
| 88 | + scan(source).map((w) => w.field), |
| 89 | + ['[whole-record-spread]', 'lastPerfProfile'], |
| 90 | + source, |
| 91 | + ); |
| 92 | +}); |
| 93 | + |
| 94 | +test('typed store aliases enforce the same explicit patch contract', () => { |
| 95 | + assert.deepEqual( |
| 96 | + scan( |
| 97 | + 'function update(storage: SessionStore) { storage.update(ref, { appLogFailure: error }); }', |
| 98 | + ).map((w) => w.field), |
| 99 | + ['appLogFailure'], |
| 100 | + ); |
| 101 | + assert.deepEqual( |
| 102 | + scan('function update(storage: SessionStore) { storage.update(ref, changes); }').map( |
| 103 | + (w) => w.field, |
| 104 | + ), |
| 105 | + ['[patch-shape]'], |
| 106 | + ); |
| 107 | +}); |
| 108 | + |
| 109 | +test('plain store and record aliases retain their owning identity', () => { |
| 110 | + assert.deepEqual( |
| 111 | + scan('const storage = sessionStore; storage.update(ref, { appLogFailure: error });').map( |
| 112 | + (w) => w.field, |
| 113 | + ), |
| 114 | + ['appLogFailure'], |
| 115 | + ); |
| 116 | + for (const source of [ |
| 117 | + 'const current = ref.session; return { ...current };', |
| 118 | + 'const current = session; return { ...current };', |
| 119 | + ]) { |
| 120 | + assert.deepEqual( |
| 121 | + scan(source).map((w) => w.field), |
| 122 | + ['[whole-record-spread]'], |
| 123 | + source, |
| 124 | + ); |
| 125 | + } |
| 126 | +}); |
| 127 | + |
| 128 | +test('destructuring preserves store and record aliases', () => { |
| 129 | + for (const pattern of ['{ session: current }', '{ session: current = fallback }']) { |
| 130 | + assert.deepEqual( |
| 131 | + scan(`const ${pattern} = ref; return { ...current, appLogFailure: error };`).map( |
| 132 | + (w) => w.field, |
| 133 | + ), |
| 134 | + ['[whole-record-spread]', 'appLogFailure'], |
| 135 | + ); |
| 136 | + } |
| 137 | + assert.deepEqual( |
| 138 | + scan('function copy({ session: current }: SessionRef) { return { ...current }; }').map( |
| 139 | + (w) => w.field, |
| 140 | + ), |
| 141 | + ['[whole-record-spread]'], |
| 142 | + ); |
| 143 | + assert.deepEqual( |
| 144 | + scan('const { sessionStore: storage } = params; storage.update(ref, changes);').map( |
| 145 | + (w) => w.field, |
| 146 | + ), |
| 147 | + ['[patch-shape]'], |
| 148 | + ); |
| 149 | +}); |
| 150 | + |
| 151 | +test('alias declarations and patch parameters are collected before checking writes', () => { |
| 152 | + assert.deepEqual( |
| 153 | + scan( |
| 154 | + 'function patch() { const nested = storage; nested.update(ref, changes); } const storage = sessionStore;', |
| 155 | + ).map((w) => w.field), |
| 156 | + ['[patch-shape]'], |
| 157 | + ); |
| 158 | + assert.deepEqual( |
| 159 | + scan( |
| 160 | + 'store.update(ref, (current) => { const entry = current; entry.lastPerfProfile = profile; return { appLogFailure: undefined }; });', |
| 161 | + ).map((w) => w.field), |
| 162 | + ['appLogFailure', 'lastPerfProfile'], |
| 163 | + ); |
| 164 | +}); |
| 165 | + |
| 166 | +test('draft exceptions cover only the declared constructors and fresh open publication', () => { |
| 167 | + for (const [file, constructor] of [ |
| 168 | + ['src/daemon/snapshot-session.ts', 'createSnapshotSession'], |
| 169 | + ['src/daemon/handlers/record-runtime.ts', 'createRecordOnlySession'], |
| 170 | + ]) { |
| 171 | + assert.deepEqual( |
| 172 | + scan( |
| 173 | + `function ${constructor}(session: SessionState) { return { ...session, appLogFailure: undefined }; }`, |
| 174 | + file, |
| 175 | + ), |
| 176 | + [], |
| 177 | + ); |
| 178 | + assert.ok( |
| 179 | + scan('function updateSession(session: SessionState) { return { ...session }; }', file).some( |
| 180 | + (w) => w.field === '[whole-record-spread]', |
| 181 | + ), |
| 182 | + ); |
| 183 | + } |
| 184 | + const open = 'src/daemon/session-lifecycle/internal/session-open-state.ts'; |
| 185 | + assert.deepEqual( |
| 186 | + scan( |
| 187 | + 'function publishOpenSession(session: SessionState) { return store.publish(address, { ...session }); }', |
| 188 | + open, |
| 189 | + ), |
| 190 | + [], |
| 191 | + ); |
| 192 | + assert.ok( |
| 193 | + scan( |
| 194 | + 'function publishOpenSession(session: SessionState) { return store.update(ref, { ...session }); }', |
| 195 | + open, |
| 196 | + ).some((w) => w.field === '[patch-shape]'), |
| 197 | + ); |
| 198 | + assert.ok( |
| 199 | + scan( |
| 200 | + 'function publishOpenSession(session: SessionState) { const next = { ...session }; return next; }', |
| 201 | + open, |
| 202 | + ).some((w) => w.field === '[whole-record-spread]'), |
| 203 | + ); |
| 204 | +}); |
| 205 | + |
| 206 | +test('the owning store can merge records, while unrelated updates and platform sessions are excluded', () => { |
| 207 | + assert.deepEqual( |
| 208 | + scan( |
| 209 | + 'sessionStore.update(ref, changes); session.appLogFailure = error;', |
| 210 | + 'src/daemon/session-store.ts', |
| 211 | + ), |
| 212 | + [], |
| 213 | + ); |
| 214 | + assert.deepEqual(scan('coordinator.update((session) => ({})); hash.update(data);'), []); |
| 215 | + assert.deepEqual( |
| 216 | + scan('return { ...session, appLogFailure: error };', 'packages/platform-apple/src/session.ts'), |
| 217 | + [], |
| 218 | + ); |
| 219 | +}); |
| 220 | + |
| 221 | +test('pressure counts capture-kit record replacement and daemon patches with the same syntax rules', () => { |
| 222 | + const declaration = |
| 223 | + 'export type SessionState = {\n appLogFailure?: Error;\n lease?: object;\n};'; |
| 224 | + const baseline = new Map([ |
| 225 | + ['src/daemon/session-state.ts', declaration], |
| 226 | + ['src/daemon/owner.ts', 'session.appLogFailure = error;'], |
| 227 | + [ |
| 228 | + 'packages/capture-kit/src/capture-admission/owner.ts', |
| 229 | + 'const next = { ...session, appLogFailure: error };', |
| 230 | + ], |
| 231 | + ]); |
| 232 | + const current = new Map([ |
| 233 | + ['src/daemon/session-state.ts', declaration], |
| 234 | + [OWNER, 'store.update(ref, { appLogFailure: error });'], |
| 235 | + ['src/daemon/invalid.ts', 'store.update(ref, changes);'], |
| 236 | + ]); |
| 237 | + assert.deepEqual(sessionStateWritePressure(baseline), { |
| 238 | + writerOwnedFields: 1, |
| 239 | + ownerFileClaims: 2, |
| 240 | + }); |
| 241 | + assert.deepEqual(sessionStateWritePressure(current), { |
| 242 | + writerOwnedFields: 1, |
| 243 | + ownerFileClaims: 1, |
| 244 | + }); |
| 245 | +}); |
0 commit comments