228 lines
7.9 KiB
JavaScript
228 lines
7.9 KiB
JavaScript
import assert from 'node:assert/strict'
|
|
import {
|
|
mkdirSync,
|
|
mkdtempSync,
|
|
readFileSync,
|
|
readdirSync,
|
|
rmSync,
|
|
statSync,
|
|
symlinkSync,
|
|
writeFileSync,
|
|
} from 'node:fs'
|
|
import { tmpdir } from 'node:os'
|
|
import { join } from 'node:path'
|
|
|
|
import {
|
|
allProfileContexts,
|
|
contextForProfile,
|
|
desktopProfileContextPath,
|
|
loadDesktopProfileContext,
|
|
persistDesktopProfileContextFile,
|
|
primaryProfilePaths,
|
|
profileKindEnvironment,
|
|
serializeDesktopProfileContext,
|
|
updateDesktopProfileContextFile,
|
|
} from '../dist/desktop-profile-context.js'
|
|
|
|
const root = mkdtempSync(join(tmpdir(), 'opensquilla-profile-context-'))
|
|
try {
|
|
const primary = primaryProfilePaths(root)
|
|
assert.equal(primary.home, join(root, 'opensquilla'))
|
|
assert.equal(primary.credentialPath, join(root, 'desktop-credential.json'))
|
|
assert.equal(primary.logsDir, join(root, 'logs'))
|
|
assert.equal(profileKindEnvironment('primary'), 'desktop-primary')
|
|
assert.equal(profileKindEnvironment('recovery'), 'desktop-recovery')
|
|
assert.equal(loadDesktopProfileContext(root).issue, null, 'a genuinely missing context is a fresh primary')
|
|
|
|
const recoveryId = '01234567-89ab-4cde-8fab-0123456789ab'
|
|
const recovery = contextForProfile(root, 'recovery', recoveryId)
|
|
mkdirSync(recovery.active.home, { recursive: true })
|
|
await persistDesktopProfileContextFile(root, recovery)
|
|
if (process.platform !== 'win32') {
|
|
assert.equal(
|
|
statSync(desktopProfileContextPath(root)).mode & 0o077,
|
|
0,
|
|
'context permissions must not grant group/other access',
|
|
)
|
|
}
|
|
assert.equal(
|
|
readdirSync(root).some((entry) => entry.includes('desktop-profile-context.json.') && entry.endsWith('.tmp')),
|
|
false,
|
|
'durable writes must not leave a temp file behind',
|
|
)
|
|
|
|
const loaded = loadDesktopProfileContext(root)
|
|
assert.equal(loaded.issue, null)
|
|
assert.equal(loaded.active.kind, 'recovery')
|
|
assert.equal(loaded.active.recoveryId, recoveryId)
|
|
assert.equal(loaded.active.home, join(root, 'recovery-profiles', recoveryId, 'opensquilla'))
|
|
assert.equal(loaded.active.credentialPath, join(root, 'recovery-profiles', recoveryId, 'desktop-credential.json'))
|
|
assert.equal(loaded.active.logsDir, join(root, 'recovery-profiles', recoveryId, 'logs'))
|
|
|
|
const profiles = allProfileContexts(root)
|
|
assert.deepEqual(profiles.map((profile) => profile.kind), ['primary', 'recovery'])
|
|
|
|
const concurrentRecoveryId = '31234567-89ab-4cde-8fab-0123456789ab'
|
|
mkdirSync(contextForProfile(root, 'recovery', concurrentRecoveryId).active.home, {
|
|
recursive: true,
|
|
})
|
|
const acknowledgement = {
|
|
stable_code: 'workspace_conflict',
|
|
candidates: [{
|
|
path: join(root, 'opensquilla', 'workspace'),
|
|
identity: '1:2',
|
|
modified_at_ns: 123,
|
|
}],
|
|
}
|
|
await Promise.all([
|
|
updateDesktopProfileContextFile(root, async (current) => {
|
|
await new Promise((resolve) => setTimeout(resolve, 20))
|
|
return contextForProfile(
|
|
root,
|
|
'recovery',
|
|
concurrentRecoveryId,
|
|
new Date().toISOString(),
|
|
current.persisted.attention_acknowledgement,
|
|
)
|
|
}),
|
|
updateDesktopProfileContextFile(root, (current) => contextForProfile(
|
|
root,
|
|
current.active.kind,
|
|
current.active.recoveryId,
|
|
new Date().toISOString(),
|
|
acknowledgement,
|
|
)),
|
|
])
|
|
const concurrentlyUpdated = loadDesktopProfileContext(root)
|
|
assert.equal(concurrentlyUpdated.active.recoveryId, concurrentRecoveryId)
|
|
assert.deepEqual(
|
|
concurrentlyUpdated.persisted.attention_acknowledgement,
|
|
acknowledgement,
|
|
'serialized read-modify-write must preserve both concurrent decisions',
|
|
)
|
|
|
|
let releaseLockedUpdate = () => {}
|
|
let markLockedUpdateStarted = () => {}
|
|
const lockedUpdateMayFinish = new Promise((resolve) => {
|
|
releaseLockedUpdate = resolve
|
|
})
|
|
const lockedUpdateStarted = new Promise((resolve) => {
|
|
markLockedUpdateStarted = resolve
|
|
})
|
|
const lockedUpdate = updateDesktopProfileContextFile(root, async (current) => {
|
|
markLockedUpdateStarted()
|
|
await lockedUpdateMayFinish
|
|
return contextForProfile(
|
|
root,
|
|
current.active.kind,
|
|
current.active.recoveryId,
|
|
new Date().toISOString(),
|
|
current.persisted.attention_acknowledgement,
|
|
)
|
|
})
|
|
await lockedUpdateStarted
|
|
let directPersistSettled = false
|
|
const directPersist = persistDesktopProfileContextFile(
|
|
root,
|
|
contextForProfile(root, 'primary'),
|
|
).then(() => {
|
|
directPersistSettled = true
|
|
})
|
|
await new Promise((resolve) => setTimeout(resolve, 50))
|
|
assert.equal(
|
|
directPersistSettled,
|
|
false,
|
|
'direct persistence must join the updater lock instead of publishing mid-transaction',
|
|
)
|
|
assert.equal(
|
|
loadDesktopProfileContext(root).active.recoveryId,
|
|
concurrentRecoveryId,
|
|
'a queued direct persistence must leave the updater snapshot untouched',
|
|
)
|
|
releaseLockedUpdate()
|
|
await lockedUpdate
|
|
await directPersist
|
|
assert.equal(loadDesktopProfileContext(root).active.kind, 'primary')
|
|
|
|
const externalChoice = contextForProfile(root, 'primary')
|
|
await assert.rejects(
|
|
updateDesktopProfileContextFile(root, async (current) => {
|
|
writeFileSync(
|
|
desktopProfileContextPath(root),
|
|
serializeDesktopProfileContext(externalChoice),
|
|
'utf8',
|
|
)
|
|
return contextForProfile(
|
|
root,
|
|
current.active.kind,
|
|
current.active.recoveryId,
|
|
new Date().toISOString(),
|
|
acknowledgement,
|
|
)
|
|
}),
|
|
/changed while it was being updated/,
|
|
)
|
|
assert.equal(
|
|
loadDesktopProfileContext(root).active.kind,
|
|
'primary',
|
|
'CAS rejection must preserve the external writer\'s newer choice',
|
|
)
|
|
|
|
const linkedId = '11234567-89ab-4cde-8fab-0123456789ab'
|
|
const outside = join(root, 'outside')
|
|
mkdirSync(outside)
|
|
const profileCountBeforeLinkedRoot = allProfileContexts(root).length
|
|
symlinkSync(outside, join(root, 'recovery-profiles', linkedId))
|
|
assert.equal(
|
|
allProfileContexts(root).length,
|
|
profileCountBeforeLinkedRoot,
|
|
'linked recovery roots must be ignored',
|
|
)
|
|
writeFileSync(desktopProfileContextPath(root), serializeDesktopProfileContext(
|
|
contextForProfile(root, 'recovery', linkedId),
|
|
), 'utf8')
|
|
assert.equal(
|
|
loadDesktopProfileContext(root).issue,
|
|
'desktop_selected_recovery_profile_unsafe',
|
|
'a selected linked recovery root must never be activated',
|
|
)
|
|
|
|
const corrupt = '{truncated'
|
|
writeFileSync(desktopProfileContextPath(root), corrupt, 'utf8')
|
|
const corruptLoaded = loadDesktopProfileContext(root)
|
|
assert.equal(corruptLoaded.active.kind, 'primary')
|
|
assert.equal(corruptLoaded.issue, 'desktop_profile_context_corrupt')
|
|
assert.equal(readFileSync(desktopProfileContextPath(root), 'utf8'), corrupt, 'inspection preserves corrupt context')
|
|
|
|
writeFileSync(desktopProfileContextPath(root), JSON.stringify({
|
|
schema_version: 2,
|
|
active_profile_kind: 'primary',
|
|
active_recovery_id: null,
|
|
attention_acknowledgement: null,
|
|
updated_at: new Date().toISOString(),
|
|
}), 'utf8')
|
|
assert.equal(
|
|
loadDesktopProfileContext(root).issue,
|
|
'desktop_profile_context_schema_too_new',
|
|
)
|
|
|
|
const missingId = '21234567-89ab-4cde-8fab-0123456789ab'
|
|
writeFileSync(desktopProfileContextPath(root), serializeDesktopProfileContext(
|
|
contextForProfile(root, 'recovery', missingId),
|
|
), 'utf8')
|
|
const missingLoaded = loadDesktopProfileContext(root)
|
|
assert.equal(missingLoaded.active.kind, 'recovery', 'the vanished selection is not silently changed')
|
|
assert.equal(missingLoaded.active.recoveryId, missingId)
|
|
assert.equal(missingLoaded.issue, 'desktop_selected_recovery_profile_missing')
|
|
|
|
// An explicit user choice is the only thing that replaces an invalid
|
|
// context. The durable write then clears the blocked selection state.
|
|
await persistDesktopProfileContextFile(root, contextForProfile(root, 'primary'))
|
|
assert.equal(loadDesktopProfileContext(root).issue, null)
|
|
assert.equal(loadDesktopProfileContext(root).active.kind, 'primary')
|
|
} finally {
|
|
rmSync(root, { recursive: true, force: true })
|
|
}
|
|
|
|
console.log('desktop profile context checks passed')
|