d3ro-voice/apps/mobile-rn/__tests__/templates-memos.local.integration.mjs
2026-08-29 18:33:45 +09:00

613 lines
24 KiB
JavaScript

import assert from 'node:assert/strict'
import { createClient } from '@supabase/supabase-js'
const supabaseUrl = process.env.D3RO_LOCAL_SUPABASE_URL?.trim() ?? ''
const anonKey = process.env.D3RO_LOCAL_SUPABASE_ANON_KEY?.trim() ?? ''
const serviceRoleKey = process.env.D3RO_LOCAL_SUPABASE_SERVICE_ROLE_KEY?.trim() ?? ''
if (!supabaseUrl.startsWith('http://127.0.0.1:55321') || anonKey.length < 20 || serviceRoleKey.length < 20) {
throw new Error('D3RO local Supabase credentials for port 55321 are required')
}
const options = {
auth: { persistSession: false, autoRefreshToken: false, detectSessionInUrl: false },
}
const admin = createClient(supabaseUrl, serviceRoleKey, options)
const anonymousClient = createClient(supabaseUrl, anonKey, options)
const userAClient = createClient(supabaseUrl, anonKey, options)
const userBClient = createClient(supabaseUrl, anonKey, options)
const suffix = `${Date.now()}-${Math.floor(Math.random() * 1_000_000)}`
const password = `D3ro-${suffix}-Strong!`
const createdUserIds = []
let assertions = 0
function checked(condition, message) {
assert.ok(condition, message)
assertions += 1
}
async function createDisposableUser(client, marker) {
const response = await client.auth.signUp({
email: `d3ro.templates.${suffix}.${marker}@gmail.com`,
password,
})
assert.equal(response.error, null, `Could not create local user ${marker}`)
assert.ok(response.data.user && response.data.session, `Local user ${marker} has no session`)
createdUserIds.push(response.data.user.id)
return response.data
}
async function countRows(table, filters) {
let query = admin.from(table).select('*', { count: 'exact', head: true })
for (const [column, value] of Object.entries(filters)) query = query.eq(column, value)
const result = await query
assert.equal(result.error, null, `Could not count ${table}`)
return result.count ?? 0
}
try {
const [userA, userB] = await Promise.all([
createDisposableUser(userAClient, 'a'),
createDisposableUser(userBClient, 'b'),
])
const userAId = userA.user.id
const userBId = userB.user.id
// Local GoTrue and PostgREST can straddle adjacent whole-second JWT clocks.
// Let the freshly issued `iat` become current before the first REST RPC.
await new Promise((resolve) => setTimeout(resolve, 1_100))
const anonymousGeneration = await fetch(`${supabaseUrl}/functions/v1/generate-meeting-document`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({}),
})
checked(anonymousGeneration.status === 401, 'Document generation accepted an anonymous request')
const invalidGeneration = await fetch(`${supabaseUrl}/functions/v1/generate-meeting-document`, {
method: 'POST',
headers: {
Authorization: `Bearer ${userA.session.access_token}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
meetingId: crypto.randomUUID(),
templateId: crypto.randomUUID(),
idempotencyKey: crypto.randomUUID(),
title: 'Invalid override',
prompt: 'Client prompts must never override the server template',
}),
})
const invalidGenerationBody = await invalidGeneration.json().catch(() => null)
checked(
invalidGeneration.status === 400 && invalidGenerationBody?.error === 'invalid_request',
'Document generation accepted unsupported client-controlled prompt fields',
)
const bootstrapRuns = await Promise.all(
Array.from({ length: 8 }, () => userAClient.rpc('bootstrap_user_templates_v1')),
)
for (const run of bootstrapRuns) assert.equal(run.error, null, 'Concurrent template bootstrap failed')
const templatesA = await userAClient
.from('user_templates')
.select('*')
.eq('user_id', userAId)
.order('builtin_key')
assert.equal(templatesA.error, null)
checked(templatesA.data.length === 7, 'Concurrent bootstrap created duplicate builtins')
checked(
new Set(templatesA.data.map((row) => `${row.template_kind}:${row.builtin_key}`)).size === 7,
'Builtin kind/key identities are not unique',
)
checked(
templatesA.data.find((row) => row.builtin_key === 'builtin-email')?.output_format
=== 'To: {{recipient}}\nSubject: {{subject}}\n\n{{body}}',
'Desktop email builtin semantics changed',
)
checked(
templatesA.data.find((row) => row.builtin_key === 'builtin-minutes')?.system_prompt
?.includes('전사록에 없는 내용을 추가하지 마세요') === true,
'Desktop minutes builtin prompt semantics changed',
)
checked(
await countRows('user_template_selections', { user_id: userAId }) === 2,
'Concurrent bootstrap did not create exactly two default selections',
)
const builtinEmail = templatesA.data.find((row) => row.builtin_key === 'builtin-email')
assert.ok(builtinEmail)
const builtinMutation = await userAClient.rpc('update_user_template_v1', {
p_template_id: builtinEmail.id,
p_expected_revision: builtinEmail.revision,
p_name: 'Tampered',
p_description: null,
p_fields: builtinEmail.fields,
p_output_format: builtinEmail.output_format,
p_system_prompt: null,
})
checked(builtinMutation.error?.code === '42501', 'Builtin template mutation was not rejected')
const customCreate = await userAClient.rpc('create_user_template_v1', {
p_template_kind: 'dictation',
p_name: 'Concurrent custom',
p_description: 'CAS verification',
p_fields: [{
id: 'content', name: 'content', label: 'Content',
promptText: 'Dictate content', required: true, maxDurationSec: 120,
}],
p_output_format: '{{content}}',
p_system_prompt: null,
})
assert.equal(customCreate.error, null, 'Could not create custom template')
const custom = customCreate.data
const strangerTemplateRead = await userBClient
.from('user_templates')
.select('id')
.eq('id', custom.id)
assert.equal(strangerTemplateRead.error, null)
checked(strangerTemplateRead.data.length === 0, 'Another account can read a private template')
const updateArgs = {
p_template_id: custom.id,
p_expected_revision: custom.revision,
p_description: 'Concurrent update',
p_fields: custom.fields,
p_output_format: custom.output_format,
p_system_prompt: null,
}
const concurrentUpdateStartedAt = Date.now()
const concurrentUpdates = await Promise.all([
userAClient.rpc('update_user_template_v1', { ...updateArgs, p_name: 'Winner A' }),
userAClient.rpc('update_user_template_v1', { ...updateArgs, p_name: 'Winner B' }),
])
const concurrentUpdateEvidence = concurrentUpdates.map((result) => ({
ok: result.error === null,
code: result.error?.code ?? null,
message: result.error?.message ?? null,
}))
checked(
concurrentUpdates.filter((result) => result.error === null).length === 1
&& concurrentUpdates.filter((result) => result.error?.code === 'PT409').length === 1,
`Template revision CAS did not admit exactly one concurrent update: ${JSON.stringify(concurrentUpdateEvidence)}`,
)
checked(
Date.now() - concurrentUpdateStartedAt < 5_000,
'Template revision conflict was retried or delayed instead of returning immediately',
)
const historyInsert = await userAClient
.from('history')
.insert({
user_id: userAId,
title: 'Private launch memo',
original_text: 'Ship only after rollback validation',
duration: 1,
mode: 'dictation',
status: 'completed',
})
.select('id')
.single()
assert.equal(historyInsert.error, null, 'Could not create memo history row')
const historyId = historyInsert.data.id
const concurrentTags = await Promise.all([
userAClient.rpc('mobile_add_memo_tag_v1', { p_history_id: historyId, p_tag: 'Launch Plan' }),
userAClient.rpc('mobile_add_memo_tag_v1', { p_history_id: historyId, p_tag: 'launch plan' }),
])
for (const result of concurrentTags) assert.equal(result.error, null, 'Concurrent tag insert failed')
checked(
concurrentTags[0].data.id === concurrentTags[1].data.id,
'Case-insensitive concurrent tag insert was not idempotent',
)
checked(
await countRows('memo_tags', { history_id: historyId }) === 1,
'Case-insensitive duplicate memo tags were stored',
)
const strangerTag = await userBClient.rpc('mobile_add_memo_tag_v1', {
p_history_id: historyId,
p_tag: 'stolen',
})
checked(strangerTag.error?.code === 'P0002', 'Another account attached a tag to private history')
const forgedDirectTag = await userBClient.from('memo_tags').insert({
user_id: userBId,
history_id: historyId,
tag: 'forged',
normalized_tag: 'forged',
})
checked(forgedDirectTag.error !== null, 'Authenticated clients retained direct memo-tag write access')
const ownerMemoSearch = await userAClient.rpc('mobile_search_memos_v1', {
p_query: 'launch', p_tag: 'LAUNCH PLAN', p_limit: 20, p_offset: 0,
})
assert.equal(ownerMemoSearch.error, null)
checked(ownerMemoSearch.data.length === 1, 'Owner tag/content search did not return the memo')
const strangerMemoSearch = await userBClient.rpc('mobile_search_memos_v1', {
p_query: 'launch', p_tag: null, p_limit: 20, p_offset: 0,
})
assert.equal(strangerMemoSearch.error, null)
checked(strangerMemoSearch.data.length === 0, 'Another account can search private memos')
const caseRename = await userAClient.rpc('mobile_rename_memo_tag_v1', {
p_old_tag: 'launch plan', p_new_tag: 'LAUNCH PLAN',
})
assert.equal(caseRename.error, null)
checked(caseRename.data === 1, 'Case-only rename did not update exactly one memo')
checked(
await countRows('memo_tags', { history_id: historyId }) === 1,
'Case-only rename removed or duplicated the tag',
)
const bootstrapB = await userBClient.rpc('bootstrap_user_templates_v1')
assert.equal(bootstrapB.error, null)
const minutesA = templatesA.data.find((row) => row.builtin_key === 'builtin-minutes')
const minutesB = bootstrapB.data.find((row) => row.builtin_key === 'builtin-minutes')
assert.ok(minutesA && minutesB)
const anonymousCreation = await anonymousClient.rpc('mobile_create_meeting_workspace_v2', {
p_title: 'Anonymous attempt',
p_attendees: [],
p_language: 'ko',
p_template_id: minutesA.id,
p_idempotency_key: crypto.randomUUID(),
})
checked(anonymousCreation.error?.code === '42501', 'Anonymous meeting creation was accepted')
const creationKey = crypto.randomUUID()
const creationArgs = {
p_title: ' H-006 launch review ',
p_attendees: ['Yun Chan', 'Product Team'],
p_language: 'EN',
p_template_id: minutesA.id,
p_idempotency_key: creationKey,
}
const concurrentCreations = await Promise.all(
Array.from({ length: 8 }, () => (
userAClient.rpc('mobile_create_meeting_workspace_v2', creationArgs)
)),
)
for (const result of concurrentCreations) {
assert.equal(result.error, null, 'Concurrent meeting creation failed')
}
const createdMeeting = concurrentCreations[0].data
checked(
concurrentCreations.every((result) => result.data.id === createdMeeting.id)
&& await countRows('meetings', {
user_id: userAId,
creation_idempotency_key: creationKey,
}) === 1,
'Concurrent meeting creation was not idempotent',
)
checked(
createdMeeting.user_id === userAId
&& createdMeeting.title === 'H-006 launch review'
&& createdMeeting.status === 'recording'
&& createdMeeting.language === 'en'
&& JSON.stringify(createdMeeting.attendees) === JSON.stringify(['Yun Chan', 'Product Team'])
&& createdMeeting.template_id === minutesA.id
&& createdMeeting.creation_idempotency_key === creationKey,
'Atomic meeting creation did not return the normalized server row',
)
const forgedCreationMetadata = await userAClient
.from('meetings')
.insert({
user_id: userAId,
title: 'Direct metadata bypass',
status: 'recording',
language: 'ja',
attendees: ['Bypass'],
template_id: minutesA.id,
})
checked(
forgedCreationMetadata.error?.code === '42501',
'Direct INSERT bypassed the atomic H-006 meeting creation RPC',
)
const forgedIdentityUpdate = await userAClient
.from('meetings')
.update({ creation_idempotency_key: crypto.randomUUID() })
.eq('id', createdMeeting.id)
checked(
forgedIdentityUpdate.error?.code === '42501',
'Direct UPDATE mutated an atomic meeting creation identity',
)
const preservedCreationIdentity = await userAClient
.from('meetings')
.select('creation_idempotency_key, creation_request_hash')
.eq('id', createdMeeting.id)
.single()
assert.equal(preservedCreationIdentity.error, null)
checked(
preservedCreationIdentity.data.creation_idempotency_key === creationKey
&& /^[0-9a-f]{64}$/.test(preservedCreationIdentity.data.creation_request_hash),
'Rejected identity mutation changed the committed creation metadata',
)
const strangerMeetingRead = await userBClient
.from('meetings')
.select('id')
.eq('id', createdMeeting.id)
assert.equal(strangerMeetingRead.error, null)
checked(strangerMeetingRead.data.length === 0, 'Another account can read a private created meeting')
const creationConflictStartedAt = Date.now()
const creationConflict = await userAClient.rpc('mobile_create_meeting_workspace_v2', {
...creationArgs,
p_title: 'Different request with the same key',
})
checked(
creationConflict.error?.code === 'PT409'
&& Date.now() - creationConflictStartedAt < 5_000,
`Meeting creation conflict did not return immediate HTTP 409 semantics: ${JSON.stringify(creationConflict.error)}`,
)
const unchangedCreation = await userAClient
.from('meetings')
.select('title, attendees, language, template_id')
.eq('id', createdMeeting.id)
.single()
assert.equal(unchangedCreation.error, null)
checked(
unchangedCreation.data.title === 'H-006 launch review'
&& unchangedCreation.data.language === 'en'
&& unchangedCreation.data.template_id === minutesA.id,
'Idempotency conflict mutated the already committed meeting',
)
const foreignTemplateCreationKey = crypto.randomUUID()
const foreignTemplateCreation = await userBClient.rpc('mobile_create_meeting_workspace_v2', {
p_title: 'Foreign template attempt',
p_attendees: [],
p_language: 'ko',
p_template_id: minutesA.id,
p_idempotency_key: foreignTemplateCreationKey,
})
checked(
foreignTemplateCreation.error?.code === 'P0002'
&& await countRows('meetings', {
user_id: userBId,
creation_idempotency_key: foreignTemplateCreationKey,
}) === 0,
'Another account created a meeting with a private template or left a partial row',
)
const invalidAttendeesCreationKey = crypto.randomUUID()
const invalidAttendeesCreation = await userAClient.rpc('mobile_create_meeting_workspace_v2', {
p_title: 'Invalid attendees rollback',
p_attendees: ['Yun Chan', 'yun chan'],
p_language: 'ko',
p_template_id: minutesA.id,
p_idempotency_key: invalidAttendeesCreationKey,
})
checked(
invalidAttendeesCreation.error?.code === '22023'
&& await countRows('meetings', {
user_id: userAId,
creation_idempotency_key: invalidAttendeesCreationKey,
}) === 0,
'Invalid meeting creation left a partial row instead of rolling back',
)
const nullTitleCreationKey = crypto.randomUUID()
const nullTitleCreation = await userAClient.rpc('mobile_create_meeting_workspace_v2', {
p_title: null,
p_attendees: [],
p_language: 'ko',
p_template_id: minutesA.id,
p_idempotency_key: nullTitleCreationKey,
})
checked(
nullTitleCreation.error?.code === '22023'
&& await countRows('meetings', {
user_id: userAId,
creation_idempotency_key: nullTitleCreationKey,
}) === 0,
'NULL meeting title bypassed validation or left a partial row',
)
const competingCreationKey = crypto.randomUUID()
const competingCreations = await Promise.all([
userAClient.rpc('mobile_create_meeting_workspace_v2', {
...creationArgs,
p_title: 'Competing payload A',
p_idempotency_key: competingCreationKey,
}),
userAClient.rpc('mobile_create_meeting_workspace_v2', {
...creationArgs,
p_title: 'Competing payload B',
p_idempotency_key: competingCreationKey,
}),
])
checked(
competingCreations.filter((result) => result.error === null).length === 1
&& competingCreations.filter((result) => result.error?.code === 'PT409').length === 1
&& await countRows('meetings', {
user_id: userAId,
creation_idempotency_key: competingCreationKey,
}) === 1,
`Different concurrent meeting payloads did not admit exactly one winner: ${JSON.stringify(competingCreations.map((result) => ({ error: result.error?.code ?? null, id: result.data?.id ?? null })))}`,
)
const referencedTemplateCreate = await userAClient.rpc('create_user_template_v1', {
p_template_kind: 'meeting_document',
p_name: 'Referenced document template',
p_description: 'Deletion must fail while a meeting references this revision',
p_fields: [],
p_output_format: null,
p_system_prompt: 'Use only the supplied transcript.',
})
assert.equal(referencedTemplateCreate.error, null, 'Could not create referenced document template')
const referencedTemplate = referencedTemplateCreate.data
const referencedMeeting = await userAClient.rpc('mobile_create_meeting_workspace_v2', {
p_title: 'Template retention proof',
p_attendees: [],
p_language: 'zh-cn',
p_template_id: referencedTemplate.id,
p_idempotency_key: crypto.randomUUID(),
})
assert.equal(referencedMeeting.error, null, 'Could not create custom-template meeting')
const referencedTemplateDelete = await userAClient.rpc('delete_user_template_v1', {
p_template_id: referencedTemplate.id,
p_expected_revision: referencedTemplate.revision,
})
checked(
referencedTemplateDelete.error?.code === '23503'
&& await countRows('user_templates', { id: referencedTemplate.id }) === 1
&& await countRows('meetings', {
id: referencedMeeting.data.id,
template_id: referencedTemplate.id,
}) === 1,
'Referenced meeting template was deleted or its selection link was lost',
)
const userBCreationKey = crypto.randomUUID()
const userBCreation = await userBClient.rpc('mobile_create_meeting_workspace_v2', {
p_title: 'Owner template trigger proof',
p_attendees: [],
p_language: 'ko',
p_template_id: minutesB.id,
p_idempotency_key: userBCreationKey,
})
assert.equal(userBCreation.error, null, 'Second account could not create its own meeting')
const forgedTemplateUpdate = await userBClient
.from('meetings')
.update({ template_id: minutesA.id })
.eq('id', userBCreation.data.id)
checked(forgedTemplateUpdate.error?.code === '42501', 'Direct update bypassed template ownership')
const preservedUserBMeeting = await userBClient
.from('meetings')
.select('template_id')
.eq('id', userBCreation.data.id)
.single()
assert.equal(preservedUserBMeeting.error, null)
checked(
preservedUserBMeeting.data.template_id === minutesB.id,
'Rejected cross-owner template update changed the meeting row',
)
const meetingInsert = await userAClient
.from('meetings')
.insert({
user_id: userAId,
title: 'Atomic generation meeting',
status: 'completed',
raw_transcript: 'Yun: Ship after every rollback check passes.',
})
.select('id')
.single()
assert.equal(meetingInsert.error, null, 'Could not create generation meeting')
const meetingId = meetingInsert.data.id
const strangerClaim = await admin.rpc('claim_meeting_document_generation_v1', {
p_actor_id: userBId,
p_idempotency_key: crypto.randomUUID(),
p_meeting_id: meetingId,
p_template_id: minutesB.id,
p_title: 'Forbidden document',
p_model: 'claude-haiku-4-5-20251001',
})
checked(strangerClaim.error?.code === '42501', 'Another account claimed a private meeting generation')
const successKey = crypto.randomUUID()
const successClaim = await admin.rpc('claim_meeting_document_generation_v1', {
p_actor_id: userAId,
p_idempotency_key: successKey,
p_meeting_id: meetingId,
p_template_id: minutesA.id,
p_title: 'Atomic minutes',
p_model: 'claude-haiku-4-5-20251001',
})
assert.equal(successClaim.error, null, 'Owner could not claim generation')
checked(successClaim.data.claimed === true && successClaim.data.transcript.includes('rollback'), 'Claim did not bind the authenticated transcript')
const successCommit = await admin.rpc('commit_meeting_document_generation_v1', {
p_actor_id: userAId,
p_idempotency_key: successKey,
p_content: '## 요약\n모든 롤백 검증 후 배포한다.',
p_latency_ms: 42,
p_input_tokens: 10,
p_output_tokens: 8,
})
assert.equal(successCommit.error, null, 'Atomic generation commit failed')
const generatedDocumentId = successCommit.data.document.id
checked(
await countRows('meeting_documents', { id: generatedDocumentId }) === 1
&& await countRows('meeting_document_generation_audit', { idempotency_key: successKey }) === 1
&& await countRows('daily_usage', { user_id: userAId, feature: 'llm_haiku' }) === 1,
'Document, usage, and audit were not committed together',
)
const idempotentCommit = await admin.rpc('commit_meeting_document_generation_v1', {
p_actor_id: userAId,
p_idempotency_key: successKey,
p_content: 'This different content must never replace the committed document.',
p_latency_ms: 99,
p_input_tokens: 99,
p_output_tokens: 99,
})
assert.equal(idempotentCommit.error, null)
checked(
idempotentCommit.data.idempotent === true
&& idempotentCommit.data.document.id === generatedDocumentId
&& await countRows('meeting_document_generation_audit', { idempotency_key: successKey }) === 1,
'Idempotent retry duplicated or replaced the committed document',
)
const rollbackKey = crypto.randomUUID()
const rollbackClaim = await admin.rpc('claim_meeting_document_generation_v1', {
p_actor_id: userAId,
p_idempotency_key: rollbackKey,
p_meeting_id: meetingId,
p_template_id: minutesA.id,
p_title: 'Rollback proof',
p_model: 'claude-haiku-4-5-20251001',
})
assert.equal(rollbackClaim.error, null)
const rollbackCommit = await admin.rpc('commit_meeting_document_generation_v1', {
p_actor_id: userAId,
p_idempotency_key: rollbackKey,
p_content: '',
p_latency_ms: 1,
p_input_tokens: 1,
p_output_tokens: 1,
})
checked(rollbackCommit.error?.code === '22023', 'Invalid provider output was accepted')
checked(
await countRows('meeting_documents', { generation_idempotency_key: rollbackKey }) === 0
&& await countRows('meeting_document_generation_audit', { idempotency_key: rollbackKey }) === 0
&& (await admin.from('daily_usage').select('count').eq('user_id', userAId).eq('feature', 'llm_haiku').single()).data.count === 1,
'Failed atomic commit left document, audit, or usage side effects',
)
const failureMark = await admin.rpc('fail_meeting_document_generation_v1', {
p_actor_id: userAId,
p_idempotency_key: rollbackKey,
p_error_code: 'provider_invalid_response',
})
assert.equal(failureMark.error, null)
checked(failureMark.data === true, 'Failed request was not terminally marked')
const providerUnavailableResponse = await fetch(`${supabaseUrl}/functions/v1/generate-meeting-document`, {
method: 'POST',
headers: {
Authorization: `Bearer ${userA.session.access_token}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
meetingId,
templateId: minutesA.id,
idempotencyKey: crypto.randomUUID(),
title: 'Provider fail-closed proof',
}),
})
const providerUnavailableBody = await providerUnavailableResponse.json().catch(() => null)
checked(
providerUnavailableResponse.status === 503
&& providerUnavailableBody?.error === 'provider_unavailable',
`Local provider-unavailable path did not fail closed with a stable error: ${providerUnavailableResponse.status} ${JSON.stringify(providerUnavailableBody)}`,
)
console.log(`templates/memos local integration passed: ${assertions} assertions`)
} finally {
await Promise.allSettled(createdUserIds.map((userId) => admin.auth.admin.deleteUser(userId)))
}