mixdog 0.9.19 → 0.9.20
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/LICENSE +21 -0
- package/README.md +93 -29
- package/package.json +3 -2
- package/scripts/build-runtime-windows.ps1 +242 -242
- package/scripts/build-tui.mjs +6 -0
- package/scripts/hook-bus-test.mjs +8 -0
- package/scripts/log-writer-guard-smoke.mjs +131 -0
- package/scripts/reactive-compact-persist-smoke.mjs +22 -6
- package/scripts/recall-bench-cases.json +0 -1
- package/scripts/recall-quality-cases.json +1 -2
- package/scripts/recall-usecase-cases.json +1 -1
- package/scripts/session-ingest-compaction-smoke.mjs +241 -0
- package/scripts/smoke-runtime-negative.ps1 +106 -106
- package/scripts/tool-efficiency-diag.mjs +1 -1
- package/scripts/tool-smoke.mjs +150 -45
- package/src/help.mjs +2 -5
- package/src/lib/mixdog-debug.cjs +13 -0
- package/src/mixdog-session-runtime.mjs +7 -3328
- package/src/rules/lead/02-channels.md +3 -3
- package/src/runtime/agent/orchestrator/context/collect.mjs +33 -9
- package/src/runtime/agent/orchestrator/session/agent-loop.mjs +663 -0
- package/src/runtime/agent/orchestrator/session/compact/engine.mjs +6 -0
- package/src/runtime/agent/orchestrator/session/eager-dispatch.mjs +153 -0
- package/src/runtime/agent/orchestrator/session/loop/recall-fasttrack.mjs +49 -0
- package/src/runtime/agent/orchestrator/session/loop/stored-tool-args.mjs +9 -1
- package/src/runtime/agent/orchestrator/session/loop.mjs +8 -1860
- package/src/runtime/agent/orchestrator/session/manager/agent-runtime-singleton.mjs +29 -0
- package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +686 -0
- package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +60 -12
- package/src/runtime/agent/orchestrator/session/manager/env-utils.mjs +8 -0
- package/src/runtime/agent/orchestrator/session/manager/idle-cleanup.mjs +159 -0
- package/src/runtime/agent/orchestrator/session/manager/message-sanitize.mjs +143 -0
- package/src/runtime/agent/orchestrator/session/manager/prefetch-bridge.mjs +268 -0
- package/src/runtime/agent/orchestrator/session/manager/provider-cache-key.mjs +22 -0
- package/src/runtime/agent/orchestrator/session/manager/runtime-loaders.mjs +26 -0
- package/src/runtime/agent/orchestrator/session/manager/session-close.mjs +124 -0
- package/src/runtime/agent/orchestrator/session/manager/session-crud.mjs +258 -0
- package/src/runtime/agent/orchestrator/session/manager/session-errors.mjs +20 -0
- package/src/runtime/agent/orchestrator/session/manager/session-id.mjs +9 -0
- package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +475 -0
- package/src/runtime/agent/orchestrator/session/manager/session-lock.mjs +23 -0
- package/src/runtime/agent/orchestrator/session/manager.mjs +88 -2285
- package/src/runtime/agent/orchestrator/session/pre-send-compact.mjs +440 -0
- package/src/runtime/agent/orchestrator/session/send-with-recovery.mjs +153 -0
- package/src/runtime/agent/orchestrator/session/store.mjs +4 -1
- package/src/runtime/agent/orchestrator/session/tool-batch.mjs +642 -0
- package/src/runtime/agent/orchestrator/tools/bash-session.mjs +23 -3
- package/src/runtime/agent/orchestrator/tools/builtin/shell-job-paths.mjs +108 -2
- package/src/runtime/agent/orchestrator/tools/builtin/shell-job-process.mjs +15 -3
- package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +7 -0
- package/src/runtime/agent/orchestrator/tools/builtin/shell-runtime.mjs +24 -2
- package/src/runtime/agent/orchestrator/tools/patch/orchestrator.mjs +2 -2
- package/src/runtime/agent/orchestrator/tools/patch/parsing.mjs +72 -8
- package/src/runtime/agent/orchestrator/tools/shell-policy-danger-target.mjs +5 -1
- package/src/runtime/channels/index.mjs +6 -2183
- package/src/runtime/channels/lib/inbound-handler.mjs +328 -0
- package/src/runtime/channels/lib/interaction-handlers.mjs +260 -0
- package/src/runtime/channels/lib/network-retry.mjs +23 -0
- package/src/runtime/channels/lib/owned-runtime.mjs +547 -0
- package/src/runtime/channels/lib/transcript-binding.mjs +336 -0
- package/src/runtime/channels/lib/voice-runtime-fetcher.mjs +39 -2
- package/src/runtime/channels/lib/worker-bootstrap.mjs +97 -0
- package/src/runtime/channels/lib/worker-ipc.mjs +201 -0
- package/src/runtime/channels/lib/worker-main.mjs +771 -0
- package/src/runtime/memory/index.mjs +73 -1725
- package/src/runtime/memory/lib/embedding-provider.mjs +4 -2
- package/src/runtime/memory/lib/embedding-worker.mjs +47 -6
- package/src/runtime/memory/lib/http-router.mjs +772 -0
- package/src/runtime/memory/lib/memory-action-handlers.mjs +901 -0
- package/src/runtime/memory/lib/memory-embed.mjs +28 -7
- package/src/runtime/memory/lib/memory-recall-store.mjs +4 -4
- package/src/runtime/memory/lib/query-handlers.mjs +2 -2
- package/src/runtime/memory/lib/session-ingest-runtime.mjs +401 -0
- package/src/session-runtime/boot-profile.mjs +36 -0
- package/src/session-runtime/channel-config-api.mjs +70 -0
- package/src/session-runtime/context-status.mjs +181 -0
- package/src/session-runtime/env.mjs +17 -0
- package/src/session-runtime/lifecycle-api.mjs +242 -0
- package/src/session-runtime/model-route-api.mjs +198 -0
- package/src/session-runtime/provider-auth-api.mjs +135 -0
- package/src/session-runtime/resource-api.mjs +282 -0
- package/src/session-runtime/runtime-core.mjs +2046 -0
- package/src/session-runtime/session-turn-api.mjs +274 -0
- package/src/session-runtime/tool-catalog.mjs +18 -264
- package/src/session-runtime/tool-defs.mjs +2 -2
- package/src/session-runtime/workflow-agents-api.mjs +238 -0
- package/src/standalone/agent-tool.mjs +2 -2
- package/src/standalone/channel-worker.mjs +4 -0
- package/src/standalone/memory-runtime-proxy.mjs +56 -6
- package/src/tui/App.jsx +39 -28
- package/src/tui/app/core-memory-picker.mjs +1 -1
- package/src/tui/app/use-mouse-input.mjs +6 -0
- package/src/tui/app/use-transcript-scroll.mjs +77 -3
- package/src/tui/dist/index.mjs +1990 -1527
- package/src/tui/engine/context-state.mjs +145 -0
- package/src/tui/engine/prompt-history.mjs +27 -0
- package/src/tui/engine/session-api-ext.mjs +478 -0
- package/src/tui/engine/session-api.mjs +545 -0
- package/src/tui/engine/session-flow.mjs +485 -0
- package/src/tui/engine/turn.mjs +1078 -0
- package/src/tui/engine.mjs +68 -2620
- package/src/tui/index.jsx +7 -0
- package/vendor/ink/build/ink.js +16 -1
- package/vendor/ink/build/output.js +30 -4
- package/vendor/ink/build/render.js +5 -0
- package/src/workflows/sequential/WORKFLOW.md +0 -51
|
@@ -102,23 +102,39 @@ try {
|
|
|
102
102
|
thrownCode = err?.code || null;
|
|
103
103
|
}
|
|
104
104
|
|
|
105
|
+
// Non-agent sessions hard-lock to recall-fasttrack (7/3 commit). This smoke has
|
|
106
|
+
// no memory subsystem registered, so ingest_session + search both fail — the
|
|
107
|
+
// exact "memory pipeline broken" condition the fail-safe must cover. Assert the
|
|
108
|
+
// fail-safe: recall-fasttrack aborts rather than dropping head behind a false
|
|
109
|
+
// "Full history is in memory" notice, so no context is silently lost and the
|
|
110
|
+
// overflow is surfaced instead of being masked by an empty summary shell.
|
|
105
111
|
assert(threw, 'askSession should surface overflow error');
|
|
106
112
|
assert(thrownCode === 'AGENT_CONTEXT_OVERFLOW', `expected AGENT_CONTEXT_OVERFLOW, got ${thrownCode}`);
|
|
107
|
-
|
|
108
|
-
|
|
113
|
+
// Recall-fasttrack aborted (memory failed), so the reactive retry never produced
|
|
114
|
+
// a smaller transcript to re-send: exactly one failing main send, no LLM compact.
|
|
115
|
+
assert(mainSendCount === 1, `expected 1 main send (reactive retry aborted by fail-safe), got ${mainSendCount}`);
|
|
116
|
+
assert(compactSendCount === 0, `expected 0 compact sends on recall-fasttrack path, got ${compactSendCount}`);
|
|
109
117
|
|
|
110
118
|
const reloaded = loadSession(sessionId);
|
|
111
119
|
assert(reloaded, 'session should reload from store after overflow');
|
|
112
|
-
|
|
120
|
+
// Fail-safe keeps full history for the cycle: current turn appended, nothing dropped.
|
|
121
|
+
assert(
|
|
122
|
+
(reloaded.messages || []).length === beforeCount + 1,
|
|
123
|
+
`full history should be preserved on fail-safe abort (expected ${beforeCount + 1}, got ${(reloaded.messages || []).length})`,
|
|
124
|
+
);
|
|
113
125
|
const summary = (reloaded.messages || []).find(
|
|
114
126
|
(m) => m?.role === 'user' && typeof m.content === 'string' && m.content.startsWith(SUMMARY_PREFIX),
|
|
115
127
|
);
|
|
116
|
-
assert(summary, '
|
|
128
|
+
assert(!summary, 'no empty summary shell should be injected when the memory pipeline fails');
|
|
129
|
+
assert(
|
|
130
|
+
!(reloaded.messages || []).some((m) => typeof m?.content === 'string' && m.content.includes('Full history is in memory')),
|
|
131
|
+
'no false "Full history is in memory" recall notice should be injected on fail-safe abort',
|
|
132
|
+
);
|
|
117
133
|
assert(
|
|
118
134
|
(reloaded.messages || []).some((m) => m?.role === 'user' && String(m.content).includes('current overflow task must stay verbatim')),
|
|
119
135
|
'current user turn should remain in persisted transcript',
|
|
120
136
|
);
|
|
121
|
-
assert(reloaded.compaction?.lastStage
|
|
122
|
-
assert(reloaded.providerState === undefined, 'providerState should clear after reactive compact
|
|
137
|
+
assert(reloaded.compaction?.lastStage === 'overflow_failed', `compaction lastStage should be overflow_failed, got ${reloaded.compaction?.lastStage}`);
|
|
138
|
+
assert(reloaded.providerState === undefined, 'providerState should clear after reactive compact abort');
|
|
123
139
|
|
|
124
140
|
process.stdout.write('reactive-compact-persist smoke passed ✓\n');
|
|
@@ -6,6 +6,5 @@
|
|
|
6
6
|
{ "id": "recent-dryrun-delta", "label": "recent-work topical: dryrun delta sum", "args": { "query": "드라이런 델타 합산", "limit": 10 }, "expect": { "kind": "results", "topNContains": ["드라이런"], "topN": 5 } },
|
|
7
7
|
{ "id": "recent-remote-singleton", "label": "recent-work topical: remote singleton seat", "args": { "query": "remote 싱글톤 좌석", "limit": 10 }, "expect": { "kind": "results", "topNContains": ["싱글톤"], "topN": 5 } },
|
|
8
8
|
{ "id": "recency-today", "label": "query+period recency: today's work (24h)", "args": { "query": "오늘 진행한 작업", "period": "24h", "limit": 10 }, "expect": { "kind": "results", "recencyOrdered": true } },
|
|
9
|
-
{ "id": "negative-projectaa", "label": "negative: every row must literally mention the term (no filler)", "args": { "query": "ProjectAA 밸런스", "limit": 10 }, "expect": { "kind": "allContain", "allContain": ["projectaa"] } },
|
|
10
9
|
{ "id": "xlang-embedding-crash", "label": "cross-language: embedding worker crash (strict topN)", "args": { "query": "embedding worker crash", "limit": 10 }, "expect": { "kind": "results", "topNContains": ["embedding"], "topN": 3 } }
|
|
11
10
|
]
|
|
@@ -5,8 +5,7 @@
|
|
|
5
5
|
{ "id": "q-recall-general", "label": "bare recall keyword", "args": { "query": "recall" }, "expect": { "kind": "results", "topNContains": ["recall"], "topN": 5 } },
|
|
6
6
|
{ "id": "q-uc4-topic-kr", "label": "UC4 topic query with Korean qualifier", "args": { "query": "recall period last 개선", "limit": 8 }, "expect": { "kind": "results", "topNContains": ["개선"], "topN": 5 } },
|
|
7
7
|
{ "id": "q-recall-improve-kr", "label": "cache improvement topic", "args": { "query": "캐시 개선" }, "expect": { "kind": "results", "topNContains": ["개선"], "topN": 5 } },
|
|
8
|
-
{ "id": "q-
|
|
9
|
-
{ "id": "q-fanout-recall-battle", "label": "fan-out array query cache+ProjectAA", "args": { "query": ["캐시 개선", "ProjectAA"], "limit": 5 }, "expect": { "kind": "results", "topNContains": ["개선", "ProjectAA"], "topN": 10 } },
|
|
8
|
+
{ "id": "q-fanout-recall-cache", "label": "fan-out array query cache+recall", "args": { "query": ["캐시 개선", "recall"], "limit": 5 }, "expect": { "kind": "results", "topNContains": ["개선", "recall"], "topN": 10 } },
|
|
10
9
|
{ "id": "q-short-2tok-cycle", "label": "short 2-token cycle1 query", "args": { "query": "cycle1 drain", "limit": 8 }, "expect": { "kind": "results", "topNContains": ["cycle1"], "topN": 3 } },
|
|
11
10
|
{ "id": "q-recall-scope-project", "label": "project-scoped recall query", "args": { "query": "recall", "cwd": "C:\\Project\\mixdog" , "limit": 10 }, "expect": { "kind": "results", "topNContains": ["recall"], "topN": 5 } }
|
|
12
11
|
]
|
|
@@ -15,4 +15,4 @@
|
|
|
15
15
|
{ "id": "uc5-yesterday", "label": "UC5 calendar yesterday", "args": { "period": "yesterday", "limit": 10 }, "expect": "browse" },
|
|
16
16
|
{ "id": "uc5-thisweek", "label": "UC5 calendar this_week", "args": { "period": "this_week", "limit": 10 }, "expect": "browse" },
|
|
17
17
|
{ "id": "uc6-category", "label": "UC6 category decision 7d", "args": { "period": "7d", "category": "decision", "limit": 10 }, "expect": "browse" }
|
|
18
|
-
]
|
|
18
|
+
]
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Compaction-append collision smoke for the session-ingest RUNTIME (not just
|
|
3
|
+
// the pure helpers). Reproduces the silent-drop bug: after compaction removes an
|
|
4
|
+
// earlier identical untimestamped message from the in-memory array, a newly
|
|
5
|
+
// APPENDED identical message used to reproduce an already-persisted ordinal →
|
|
6
|
+
// same source_ref → INSERT ... ON CONFLICT DO NOTHING silently dropped it.
|
|
7
|
+
//
|
|
8
|
+
// Proves, against an in-memory fake DB that enforces the source_ref uniqueness
|
|
9
|
+
// the real ON CONFLICT relies on:
|
|
10
|
+
// (1) post-compaction appended untimestamped identical msg → NEW row;
|
|
11
|
+
// (2) full identical re-ingest in a FRESH runtime (rows already in DB) → 0 dups;
|
|
12
|
+
// (3) subset re-ingest reproduces the same source_refs as a full re-ingest.
|
|
13
|
+
import { createSessionIngestRuntime } from '../src/runtime/memory/lib/session-ingest-runtime.mjs'
|
|
14
|
+
|
|
15
|
+
function assert(condition, message) {
|
|
16
|
+
if (!condition) throw new Error(message)
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// Minimal fake DB. Only the two statements the ingest loop issues are modelled:
|
|
20
|
+
// • SELECT COALESCE(MAX(source_turn) ...) → per-session max
|
|
21
|
+
// • INSERT INTO entries(...) ON CONFLICT DO NOTHING → dedupe on source_ref
|
|
22
|
+
// Any other query (the post-ingest raw-embedding flush's schema calls) returns
|
|
23
|
+
// an empty result; flushRawEmbeddings then hits `db._pool` (undefined) and its
|
|
24
|
+
// error is swallowed by the runtime, so the smoke never blocks on embeddings.
|
|
25
|
+
function createFakeDb() {
|
|
26
|
+
const rows = [] // { ts, role, content, source_ref, session_id, source_turn, project_id }
|
|
27
|
+
const bySourceRef = new Set()
|
|
28
|
+
let nextId = 1
|
|
29
|
+
return {
|
|
30
|
+
rows,
|
|
31
|
+
async query(sql, params = []) {
|
|
32
|
+
if (/MAX\(source_turn\)/.test(sql)) {
|
|
33
|
+
const sessionId = params[0]
|
|
34
|
+
let max = 0
|
|
35
|
+
for (const r of rows) if (r.session_id === sessionId && r.source_turn > max) max = r.source_turn
|
|
36
|
+
return { rows: [{ max_turn: max }] }
|
|
37
|
+
}
|
|
38
|
+
if (/INSERT INTO entries/.test(sql)) {
|
|
39
|
+
const [ts, role, content, source_ref, session_id, source_turn, project_id] = params
|
|
40
|
+
if (bySourceRef.has(source_ref)) return { rowCount: 0 } // ON CONFLICT DO NOTHING
|
|
41
|
+
bySourceRef.add(source_ref)
|
|
42
|
+
rows.push({ id: nextId++, ts, role, content, source_ref, session_id, source_turn, project_id })
|
|
43
|
+
return { rowCount: 1 }
|
|
44
|
+
}
|
|
45
|
+
return { rows: [] }
|
|
46
|
+
},
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Durable high-water store fake (mirrors the DB `meta` kv the facade wires). A
|
|
51
|
+
// SHARED store passed to two runtime instances simulates the state file
|
|
52
|
+
// surviving a process restart; an absent store simulates a deleted state file.
|
|
53
|
+
function makeRuntime(db, durable = new Map()) {
|
|
54
|
+
return createSessionIngestRuntime({
|
|
55
|
+
getDb: () => db,
|
|
56
|
+
log: () => {},
|
|
57
|
+
parseTsToMs: (v) => (typeof v === 'number' ? v : Number(v) || 0),
|
|
58
|
+
loadOrdinalHighWater: async (sessionId) => durable.get(sessionId) ?? null,
|
|
59
|
+
saveOrdinalHighWater: (sessionId, obj) => { durable.set(sessionId, obj); },
|
|
60
|
+
})
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const SESSION = 'compaction-smoke-sess'
|
|
64
|
+
// Untimestamped identical turns (no ts/timestamp → ordinal is what disambiguates).
|
|
65
|
+
const mk = (role, content) => ({ role, content })
|
|
66
|
+
|
|
67
|
+
// ── Scenario: live process ingests, compaction drops an earlier copy, append ──
|
|
68
|
+
const db = createFakeDb()
|
|
69
|
+
const rt = makeRuntime(db)
|
|
70
|
+
|
|
71
|
+
// Distinct object identities so compaction survival is by-reference (mirrors the
|
|
72
|
+
// runtime's immutable-transcript assumption).
|
|
73
|
+
const other1 = mk('assistant', 'sure, working on it')
|
|
74
|
+
const dupA = mk('user', 'run it again') // 1st identical untimestamped turn
|
|
75
|
+
const other2 = mk('assistant', 'done')
|
|
76
|
+
const dupB = mk('user', 'run it again') // 2nd identical untimestamped turn (distinct object)
|
|
77
|
+
|
|
78
|
+
// Call 1 (COLD): full array with two identical untimestamped user turns.
|
|
79
|
+
const arr1 = [other1, dupA, other2, dupB]
|
|
80
|
+
let r1 = await rt.ingestSessionMessages({ sessionId: SESSION, messages: arr1, limit: 5000 })
|
|
81
|
+
const userRows1 = db.rows.filter(r => r.role === 'user' && r.content === 'run it again')
|
|
82
|
+
assert(userRows1.length === 2, `cold ingest must persist BOTH identical untimestamped turns (got ${userRows1.length})`)
|
|
83
|
+
const refDupA = userRows1[0].source_ref
|
|
84
|
+
const refDupB = userRows1[1].source_ref
|
|
85
|
+
assert(refDupA !== refDupB, 'the two identical untimestamped turns must persist under distinct source_refs')
|
|
86
|
+
|
|
87
|
+
// Compaction: drop the FIRST identical copy (dupA) and some other rows; KEEP the
|
|
88
|
+
// second identical copy object (dupB) — this is what makes the array position of
|
|
89
|
+
// the surviving/new identical turn shift down onto an already-persisted ordinal.
|
|
90
|
+
// Then APPEND a genuinely new identical turn (distinct object).
|
|
91
|
+
const dupC = mk('user', 'run it again') // NEW appended identical turn
|
|
92
|
+
const arr2 = [dupB, mk('assistant', 'ok next'), dupC]
|
|
93
|
+
|
|
94
|
+
// Call 2 (WARM, same runtime/process): the appended dupC must persist as a NEW row.
|
|
95
|
+
let r2 = await rt.ingestSessionMessages({ sessionId: SESSION, messages: arr2, limit: 5000 })
|
|
96
|
+
const userRows2 = db.rows.filter(r => r.role === 'user' && r.content === 'run it again')
|
|
97
|
+
assert(
|
|
98
|
+
userRows2.length === 3,
|
|
99
|
+
`INVARIANT 1: post-compaction appended identical turn must persist as a NEW row (expected 3 total, got ${userRows2.length}) — this is the silent-drop bug`,
|
|
100
|
+
)
|
|
101
|
+
const refDupC = userRows2.find(r => r.source_ref !== refDupA && r.source_ref !== refDupB)?.source_ref
|
|
102
|
+
assert(refDupC, 'appended identical turn must have a fresh source_ref above the persisted high-water')
|
|
103
|
+
// dupB survived: it must have deduped (reused its recorded ordinal), NOT minted a new row.
|
|
104
|
+
assert(db.rows.filter(r => r.source_ref === refDupB).length === 1, 'survived identical turn must dedupe, not duplicate')
|
|
105
|
+
|
|
106
|
+
// ── INVARIANT 2: fresh runtime (new process), full identical re-ingest ────────
|
|
107
|
+
// Rows already in DB; re-ingesting the CURRENT array from start=0 must create
|
|
108
|
+
// ZERO new rows.
|
|
109
|
+
const rowCountBefore = db.rows.length
|
|
110
|
+
const rtFresh = makeRuntime(db) // cold state for this "process"
|
|
111
|
+
await rtFresh.ingestSessionMessages({ sessionId: SESSION, messages: arr2, limit: 5000 })
|
|
112
|
+
assert(
|
|
113
|
+
db.rows.length === rowCountBefore,
|
|
114
|
+
`INVARIANT 2: full identical re-ingest in a fresh process must create 0 duplicates (before=${rowCountBefore} after=${db.rows.length})`,
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
// ── INVARIANT 3: subset re-ingest reproduces the same source_refs as full ─────
|
|
118
|
+
// Fresh DB + fresh runtime; ingest the full array, capture refs; then a NEW
|
|
119
|
+
// fresh DB/runtime ingesting the same array via a small window (subset, seeded
|
|
120
|
+
// from the prefix) must reproduce the identical source_refs.
|
|
121
|
+
function refsFor(messages, opts) {
|
|
122
|
+
const d = createFakeDb()
|
|
123
|
+
const r = makeRuntime(d)
|
|
124
|
+
return { d, r }
|
|
125
|
+
}
|
|
126
|
+
{
|
|
127
|
+
const fullMsgs = [mk('user', 'x'), mk('assistant', 'y'), mk('user', 'x'), mk('user', 'x'), mk('assistant', 'y')]
|
|
128
|
+
const { d: dFull, r: rFull } = refsFor()
|
|
129
|
+
await rFull.ingestSessionMessages({ sessionId: 'subset-sess', messages: fullMsgs, limit: 5000 })
|
|
130
|
+
const fullRefs = dFull.rows.filter(r => r.session_id === 'subset-sess').map(r => r.source_ref).sort()
|
|
131
|
+
|
|
132
|
+
// Subset: fresh process, window = last 2 messages, prefix seeded internally.
|
|
133
|
+
const { d: dSub, r: rSub } = refsFor()
|
|
134
|
+
await rSub.ingestSessionMessages({ sessionId: 'subset-sess', messages: fullMsgs, limit: 2 })
|
|
135
|
+
const subRefs = dSub.rows.filter(r => r.session_id === 'subset-sess').map(r => r.source_ref)
|
|
136
|
+
for (const ref of subRefs) {
|
|
137
|
+
assert(fullRefs.includes(ref), `INVARIANT 3: subset re-ingest source_ref ${ref} must match a full re-ingest ref`)
|
|
138
|
+
}
|
|
139
|
+
// The subset window's last two messages are `x`(3rd occurrence) and `y`(2nd);
|
|
140
|
+
// their refs must equal the full re-ingest's 3rd `x` and 2nd `y` refs.
|
|
141
|
+
assert(subRefs.length === 2, `subset window should ingest exactly its 2 messages (got ${subRefs.length})`)
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// ── INVARIANT 1 across RESTART: fresh runtime (new process), same DB ──────────
|
|
145
|
+
// A restart = a NEW runtime instance (empty in-memory ordinal state AND empty
|
|
146
|
+
// WeakMap) ingesting a reloaded transcript against the SAME DB that already
|
|
147
|
+
// holds prior rows. When the reloaded (compacted) array still RETAINS the
|
|
148
|
+
// identical copies, positional ordinals reproduce each survivor's OWN live row,
|
|
149
|
+
// and a freshly appended identical turn takes a free ordinal and persists NEW.
|
|
150
|
+
{
|
|
151
|
+
const rdb = createFakeDb()
|
|
152
|
+
const S = 'restart-sess'
|
|
153
|
+
const rtA = makeRuntime(rdb)
|
|
154
|
+
await rtA.ingestSessionMessages({ sessionId: S, messages: [mk('user', 'ping'), mk('assistant', 'pong'), mk('user', 'ping')], limit: 5000 })
|
|
155
|
+
const pingBefore = rdb.rows.filter(r => r.content === 'ping')
|
|
156
|
+
assert(pingBefore.length === 2, `pre-restart should persist 2 identical pings (got ${pingBefore.length})`)
|
|
157
|
+
const beforeIds = new Set(pingBefore.map(r => r.id))
|
|
158
|
+
|
|
159
|
+
// Restart: brand-new runtime (fresh WeakMap + ordinal state), same DB. The
|
|
160
|
+
// reloaded compacted array RETAINS both ping copies (compaction dropped only
|
|
161
|
+
// the non-identical `pong`); append a genuinely new identical ping. The
|
|
162
|
+
// reloaded objects are DISTINCT references from the pre-restart ones.
|
|
163
|
+
const rtB = makeRuntime(rdb)
|
|
164
|
+
await rtB.ingestSessionMessages({ sessionId: S, messages: [mk('user', 'ping'), mk('user', 'ping'), mk('user', 'ping')], limit: 5000 })
|
|
165
|
+
const pingAfter = rdb.rows.filter(r => r.content === 'ping')
|
|
166
|
+
assert(pingAfter.length === 3, `RESTART: appended identical turn must persist as a NEW row (expected 3, got ${pingAfter.length})`)
|
|
167
|
+
// Row-identity (not ref-membership) check: the survivor re-ingest must DEDUPE
|
|
168
|
+
// onto the 2 pre-existing live rows (same ids, no re-insert) and add EXACTLY
|
|
169
|
+
// one NEW row (the append) — proving survivors mapped to live rows, not dropped.
|
|
170
|
+
const survivorRows = pingAfter.filter(r => beforeIds.has(r.id))
|
|
171
|
+
const newRows = pingAfter.filter(r => !beforeIds.has(r.id))
|
|
172
|
+
assert(survivorRows.length === 2, `RESTART: survivors must reuse the 2 live rows (got ${survivorRows.length})`)
|
|
173
|
+
assert(newRows.length === 1, `RESTART: exactly one NEW appended row expected (got ${newRows.length})`)
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// ── INVARIANT 1 across RESTART, LATER-WARM append (durable high-water) ────────
|
|
177
|
+
// The path the reviewer disproved: after restart the cold replay of a COMPACTED
|
|
178
|
+
// array seeds occNext = survivor-count T, but the DB holds K>T copies. A NEW
|
|
179
|
+
// untimestamped identical turn arriving in a LATER (warm) call is distinguishable
|
|
180
|
+
// (it is genuinely new, not one of the cold-replay survivors) and MUST persist —
|
|
181
|
+
// only the durable per-identity high-water K makes that possible.
|
|
182
|
+
{
|
|
183
|
+
const durable = new Map() // survives the "restart" (shared across runtimes)
|
|
184
|
+
const rdb = createFakeDb() // DB rows survive the restart too
|
|
185
|
+
const S = 'restart-warm-sess'
|
|
186
|
+
|
|
187
|
+
// Pre-restart process: build K=3 persisted copies of an identical untimestamped
|
|
188
|
+
// turn (cold [X,other,X] → 2 copies; then in-process compaction+append → 3rd).
|
|
189
|
+
const rtA = makeRuntime(rdb, durable)
|
|
190
|
+
const xa1 = mk('user', 'again'); const xa2 = mk('user', 'again')
|
|
191
|
+
await rtA.ingestSessionMessages({ sessionId: S, messages: [xa1, mk('assistant', 'ok'), xa2], limit: 5000 })
|
|
192
|
+
const xa3 = mk('user', 'again') // in-process appended (compaction kept xa2)
|
|
193
|
+
await rtA.ingestSessionMessages({ sessionId: S, messages: [xa2, mk('assistant', 'ok2'), xa3], limit: 5000 })
|
|
194
|
+
assert(rdb.rows.filter(r => r.content === 'again').length === 3, 'pre-restart should hold K=3 identical copies')
|
|
195
|
+
assert(durable.has(S), 'durable high-water must have been persisted for the duplicate identity')
|
|
196
|
+
|
|
197
|
+
// Restart: fresh runtime (empty WeakMap + ordinal state), SAME db + durable.
|
|
198
|
+
// Reloaded COMPACTED array retains only ONE copy (T=1).
|
|
199
|
+
const rtB = makeRuntime(rdb, durable)
|
|
200
|
+
const rx = mk('user', 'again') // reloaded survivor (distinct object)
|
|
201
|
+
await rtB.ingestSessionMessages({ sessionId: S, messages: [rx], limit: 5000 }) // COLD replay (T=1)
|
|
202
|
+
assert(rdb.rows.filter(r => r.content === 'again').length === 3, 'cold replay of survivor must not add rows (dedupe)')
|
|
203
|
+
|
|
204
|
+
// LATER warm call on rtB appends a genuinely new identical turn.
|
|
205
|
+
const rxNew = mk('user', 'again')
|
|
206
|
+
const rowsBeforeAppend = rdb.rows.length
|
|
207
|
+
await rtB.ingestSessionMessages({ sessionId: S, messages: [rx, rxNew], limit: 5000 })
|
|
208
|
+
const againAfter = rdb.rows.filter(r => r.content === 'again')
|
|
209
|
+
assert(
|
|
210
|
+
againAfter.length === 4,
|
|
211
|
+
`LATER-WARM RESTART: appended identical turn must persist as a NEW row via durable K (expected 4, got ${againAfter.length})`,
|
|
212
|
+
)
|
|
213
|
+
assert(rdb.rows.length === rowsBeforeAppend + 1, 'exactly one new row from the later-warm append')
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// ── Zero-dup guarantee when a compacted reload DROPPED an identical copy ──────
|
|
217
|
+
// If the reloaded array is missing an earlier identical untimestamped copy AND
|
|
218
|
+
// the WeakMap survivor signal is gone (restart), the appended identical turn is
|
|
219
|
+
// information-theoretically indistinguishable from the survivors (see the
|
|
220
|
+
// DURABILITY note in session-ingest-runtime.mjs). Contract in that corner: NEVER
|
|
221
|
+
// mint a duplicate survivor row (ON CONFLICT dedupes). The append may collapse;
|
|
222
|
+
// it must not duplicate.
|
|
223
|
+
{
|
|
224
|
+
const rdb = createFakeDb()
|
|
225
|
+
const S = 'restart-drop-sess'
|
|
226
|
+
const rtA = makeRuntime(rdb)
|
|
227
|
+
await rtA.ingestSessionMessages({ sessionId: S, messages: [mk('user', 'echo'), mk('user', 'echo')], limit: 5000 })
|
|
228
|
+
const before = rdb.rows.filter(r => r.content === 'echo').length
|
|
229
|
+
assert(before === 2, `pre-restart should persist 2 identical echoes (got ${before})`)
|
|
230
|
+
const rowsBefore = rdb.rows.length
|
|
231
|
+
|
|
232
|
+
// Restart with a compacted reload that DROPPED one echo, then appended one
|
|
233
|
+
// (net 2 copies again). Must not mint a duplicate; the append collapses.
|
|
234
|
+
const rtB = makeRuntime(rdb)
|
|
235
|
+
await rtB.ingestSessionMessages({ sessionId: S, messages: [mk('user', 'echo'), mk('user', 'echo')], limit: 5000 })
|
|
236
|
+
const after = rdb.rows.filter(r => r.content === 'echo').length
|
|
237
|
+
assert(after === before, `RESTART+drop: must not mint a duplicate survivor row (before=${before} after=${after})`)
|
|
238
|
+
assert(rdb.rows.length === rowsBefore, 'RESTART+drop: no duplicate rows created')
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
process.stdout.write('session ingest compaction-append smoke passed \u2713\n')
|
|
@@ -1,106 +1,106 @@
|
|
|
1
|
-
# smoke-runtime-negative.ps1 — Negative-path tests against released win32-x64
|
|
2
|
-
# runtime tarball. Validates: download, sha256, corrupt-tarball, fresh-extract
|
|
3
|
-
# boot, double-init refusal, hostile-env survival.
|
|
4
|
-
#
|
|
5
|
-
# Env: TAG / OS / ARCH / RUNTIME_RELEASE_REPOSITORY (default tribgames/mixdog)
|
|
6
|
-
|
|
7
|
-
$ErrorActionPreference = 'Stop'
|
|
8
|
-
|
|
9
|
-
$Tag = if ($env:TAG) { $env:TAG } else { 'runtime-v0.4.0' }
|
|
10
|
-
$Os = if ($env:OS) { $env:OS } else { 'win32' }
|
|
11
|
-
$Arch = if ($env:ARCH) { $env:ARCH } else { 'x64' }
|
|
12
|
-
$ReleaseRepo = if ($env:RUNTIME_RELEASE_REPOSITORY) { $env:RUNTIME_RELEASE_REPOSITORY } else { 'tribgames/mixdog' }
|
|
13
|
-
$PgVer = '16.4'
|
|
14
|
-
$PgvectorVer = '0.8.2'
|
|
15
|
-
|
|
16
|
-
$Asset = "mixdog-runtime-${Os}-${Arch}-pg${PgVer}-pgvector${PgvectorVer}.tar.gz"
|
|
17
|
-
$Url = "https://github.com/${ReleaseRepo}/releases/download/${Tag}/${Asset}"
|
|
18
|
-
|
|
19
|
-
$Work = New-Item -ItemType Directory -Force -Path "$env:TEMP\smoke-$([guid]::NewGuid().ToString('N'))" | Select-Object -ExpandProperty FullName
|
|
20
|
-
|
|
21
|
-
try {
|
|
22
|
-
Write-Host "==> Negative-path smoke for ${Os}-${Arch} from $Url"
|
|
23
|
-
|
|
24
|
-
Write-Host "==> Test 1: HEAD reaches release asset"
|
|
25
|
-
$resp = Invoke-WebRequest -Uri $Url -Method Head -UseBasicParsing -ErrorAction Stop
|
|
26
|
-
Write-Host " HTTP $($resp.StatusCode)"
|
|
27
|
-
|
|
28
|
-
Write-Host "==> Test 2: full download + sha256"
|
|
29
|
-
$TarPath = Join-Path $Work $Asset
|
|
30
|
-
Invoke-WebRequest -Uri $Url -OutFile $TarPath -UseBasicParsing
|
|
31
|
-
$sha = (Get-FileHash -Algorithm SHA256 $TarPath).Hash.ToLower()
|
|
32
|
-
Write-Host " sha256=$sha"
|
|
33
|
-
|
|
34
|
-
Write-Host "==> Test 3: corrupt tarball — flip a byte and ensure tar errors"
|
|
35
|
-
$CorruptPath = "$Work\corrupt.tar.gz"
|
|
36
|
-
Copy-Item $TarPath $CorruptPath
|
|
37
|
-
$bytes = [byte[]]::new(1); $rand = [System.Random]::new(); $rand.NextBytes($bytes)
|
|
38
|
-
$stream = [System.IO.File]::OpenWrite($CorruptPath)
|
|
39
|
-
$stream.Position = 102400
|
|
40
|
-
$stream.Write($bytes, 0, 1)
|
|
41
|
-
$stream.Close()
|
|
42
|
-
$CorruptDir = "$Work\corrupt"
|
|
43
|
-
New-Item -ItemType Directory -Force -Path $CorruptDir | Out-Null
|
|
44
|
-
$TarProc = Start-Process -FilePath tar -ArgumentList @('-xzf', $CorruptPath, '-C', $CorruptDir.Replace('\','/')) -Wait:$false -PassThru -WindowStyle Hidden
|
|
45
|
-
if (-not $TarProc.WaitForExit(15000)) {
|
|
46
|
-
Stop-Process -Id $TarProc.Id -Force -ErrorAction SilentlyContinue
|
|
47
|
-
Write-Host " PASS: corrupted tarball extraction timed out and was stopped"
|
|
48
|
-
} elseif ($TarProc.ExitCode -eq 0) {
|
|
49
|
-
Write-Host " WARN: corrupted tarball extracted without error (tar gzip recovery)"
|
|
50
|
-
} else {
|
|
51
|
-
Write-Host " PASS: corrupted tarball rejected by tar (exit $($TarProc.ExitCode))"
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
Write-Host "==> Test 4: fresh-extract boot (extension load + distance query)"
|
|
55
|
-
$FreshDir = "$Work\fresh"
|
|
56
|
-
New-Item -ItemType Directory -Force -Path $FreshDir | Out-Null
|
|
57
|
-
& tar -xzf $TarPath -C ($FreshDir.Replace('\','/'))
|
|
58
|
-
if ($LASTEXITCODE -ne 0) { throw "fresh extract failed" }
|
|
59
|
-
|
|
60
|
-
$PgBin = "$FreshDir\bin"
|
|
61
|
-
$Data = "$FreshDir\pgdata"
|
|
62
|
-
$Log = "$FreshDir\pg.log"
|
|
63
|
-
$Port = 55897
|
|
64
|
-
|
|
65
|
-
# Hostile env: minimal PATH (System32 only), no PGROOT/PGDATA
|
|
66
|
-
$SavedPath = $env:PATH
|
|
67
|
-
$SavedPgRoot = $env:PGROOT
|
|
68
|
-
$env:PATH = "$env:SystemRoot\System32;$env:SystemRoot"
|
|
69
|
-
$env:PGROOT = $null
|
|
70
|
-
$env:PGDATA = $null
|
|
71
|
-
|
|
72
|
-
try {
|
|
73
|
-
& "$PgBin\postgres.exe" --version
|
|
74
|
-
if ($LASTEXITCODE -ne 0) { throw "FAIL: postgres --version" }
|
|
75
|
-
& "$PgBin\initdb.exe" -D $Data --username=postgres --auth-local=trust --no-locale -E UTF8 | Out-Null
|
|
76
|
-
if ($LASTEXITCODE -ne 0) { throw "FAIL: initdb" }
|
|
77
|
-
& "$PgBin\pg_ctl.exe" -D $Data -o "-p $Port -h 127.0.0.1" -l $Log -w start
|
|
78
|
-
if ($LASTEXITCODE -ne 0) { Get-Content $Log | Select-Object -Last 30; throw "FAIL: pg_ctl start" }
|
|
79
|
-
|
|
80
|
-
try {
|
|
81
|
-
& "$PgBin\psql.exe" -h 127.0.0.1 -p $Port -U postgres -d postgres -c "CREATE EXTENSION vector;" | Out-Null
|
|
82
|
-
if ($LASTEXITCODE -ne 0) { throw "FAIL: CREATE EXTENSION" }
|
|
83
|
-
$ExtV = & "$PgBin\psql.exe" -h 127.0.0.1 -p $Port -U postgres -d postgres -tAc "SELECT extversion FROM pg_extension WHERE extname='vector';"
|
|
84
|
-
if ($ExtV.Trim() -ne $PgvectorVer) { throw "FAIL: extversion='$ExtV' expected='$PgvectorVer'" }
|
|
85
|
-
Write-Host " PASS: fresh-extract boot + vector extension"
|
|
86
|
-
} finally {
|
|
87
|
-
& "$PgBin\pg_ctl.exe" -D $Data -m fast stop 2>$null | Out-Null
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
Write-Host "==> Test 5: second initdb on initialized dir refused"
|
|
91
|
-
$rc = (Start-Process -FilePath "$PgBin\initdb.exe" -ArgumentList "-D",$Data,"-U","postgres" -Wait -PassThru -NoNewWindow).ExitCode
|
|
92
|
-
if ($rc -eq 0) {
|
|
93
|
-
Write-Host " WARN: second initdb succeeded (unexpected)"
|
|
94
|
-
} else {
|
|
95
|
-
Write-Host " PASS: second initdb refused (exit $rc)"
|
|
96
|
-
}
|
|
97
|
-
} finally {
|
|
98
|
-
$env:PATH = $SavedPath
|
|
99
|
-
$env:PGROOT = $SavedPgRoot
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
Write-Host "==> All negative-path smokes: PASS"
|
|
103
|
-
}
|
|
104
|
-
finally {
|
|
105
|
-
Remove-Item -Recurse -Force $Work -ErrorAction SilentlyContinue
|
|
106
|
-
}
|
|
1
|
+
# smoke-runtime-negative.ps1 — Negative-path tests against released win32-x64
|
|
2
|
+
# runtime tarball. Validates: download, sha256, corrupt-tarball, fresh-extract
|
|
3
|
+
# boot, double-init refusal, hostile-env survival.
|
|
4
|
+
#
|
|
5
|
+
# Env: TAG / OS / ARCH / RUNTIME_RELEASE_REPOSITORY (default tribgames/mixdog)
|
|
6
|
+
|
|
7
|
+
$ErrorActionPreference = 'Stop'
|
|
8
|
+
|
|
9
|
+
$Tag = if ($env:TAG) { $env:TAG } else { 'runtime-v0.4.0' }
|
|
10
|
+
$Os = if ($env:OS) { $env:OS } else { 'win32' }
|
|
11
|
+
$Arch = if ($env:ARCH) { $env:ARCH } else { 'x64' }
|
|
12
|
+
$ReleaseRepo = if ($env:RUNTIME_RELEASE_REPOSITORY) { $env:RUNTIME_RELEASE_REPOSITORY } else { 'tribgames/mixdog' }
|
|
13
|
+
$PgVer = '16.4'
|
|
14
|
+
$PgvectorVer = '0.8.2'
|
|
15
|
+
|
|
16
|
+
$Asset = "mixdog-runtime-${Os}-${Arch}-pg${PgVer}-pgvector${PgvectorVer}.tar.gz"
|
|
17
|
+
$Url = "https://github.com/${ReleaseRepo}/releases/download/${Tag}/${Asset}"
|
|
18
|
+
|
|
19
|
+
$Work = New-Item -ItemType Directory -Force -Path "$env:TEMP\smoke-$([guid]::NewGuid().ToString('N'))" | Select-Object -ExpandProperty FullName
|
|
20
|
+
|
|
21
|
+
try {
|
|
22
|
+
Write-Host "==> Negative-path smoke for ${Os}-${Arch} from $Url"
|
|
23
|
+
|
|
24
|
+
Write-Host "==> Test 1: HEAD reaches release asset"
|
|
25
|
+
$resp = Invoke-WebRequest -Uri $Url -Method Head -UseBasicParsing -ErrorAction Stop
|
|
26
|
+
Write-Host " HTTP $($resp.StatusCode)"
|
|
27
|
+
|
|
28
|
+
Write-Host "==> Test 2: full download + sha256"
|
|
29
|
+
$TarPath = Join-Path $Work $Asset
|
|
30
|
+
Invoke-WebRequest -Uri $Url -OutFile $TarPath -UseBasicParsing
|
|
31
|
+
$sha = (Get-FileHash -Algorithm SHA256 $TarPath).Hash.ToLower()
|
|
32
|
+
Write-Host " sha256=$sha"
|
|
33
|
+
|
|
34
|
+
Write-Host "==> Test 3: corrupt tarball — flip a byte and ensure tar errors"
|
|
35
|
+
$CorruptPath = "$Work\corrupt.tar.gz"
|
|
36
|
+
Copy-Item $TarPath $CorruptPath
|
|
37
|
+
$bytes = [byte[]]::new(1); $rand = [System.Random]::new(); $rand.NextBytes($bytes)
|
|
38
|
+
$stream = [System.IO.File]::OpenWrite($CorruptPath)
|
|
39
|
+
$stream.Position = 102400
|
|
40
|
+
$stream.Write($bytes, 0, 1)
|
|
41
|
+
$stream.Close()
|
|
42
|
+
$CorruptDir = "$Work\corrupt"
|
|
43
|
+
New-Item -ItemType Directory -Force -Path $CorruptDir | Out-Null
|
|
44
|
+
$TarProc = Start-Process -FilePath tar -ArgumentList @('-xzf', $CorruptPath, '-C', $CorruptDir.Replace('\','/')) -Wait:$false -PassThru -WindowStyle Hidden
|
|
45
|
+
if (-not $TarProc.WaitForExit(15000)) {
|
|
46
|
+
Stop-Process -Id $TarProc.Id -Force -ErrorAction SilentlyContinue
|
|
47
|
+
Write-Host " PASS: corrupted tarball extraction timed out and was stopped"
|
|
48
|
+
} elseif ($TarProc.ExitCode -eq 0) {
|
|
49
|
+
Write-Host " WARN: corrupted tarball extracted without error (tar gzip recovery)"
|
|
50
|
+
} else {
|
|
51
|
+
Write-Host " PASS: corrupted tarball rejected by tar (exit $($TarProc.ExitCode))"
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
Write-Host "==> Test 4: fresh-extract boot (extension load + distance query)"
|
|
55
|
+
$FreshDir = "$Work\fresh"
|
|
56
|
+
New-Item -ItemType Directory -Force -Path $FreshDir | Out-Null
|
|
57
|
+
& tar -xzf $TarPath -C ($FreshDir.Replace('\','/'))
|
|
58
|
+
if ($LASTEXITCODE -ne 0) { throw "fresh extract failed" }
|
|
59
|
+
|
|
60
|
+
$PgBin = "$FreshDir\bin"
|
|
61
|
+
$Data = "$FreshDir\pgdata"
|
|
62
|
+
$Log = "$FreshDir\pg.log"
|
|
63
|
+
$Port = 55897
|
|
64
|
+
|
|
65
|
+
# Hostile env: minimal PATH (System32 only), no PGROOT/PGDATA
|
|
66
|
+
$SavedPath = $env:PATH
|
|
67
|
+
$SavedPgRoot = $env:PGROOT
|
|
68
|
+
$env:PATH = "$env:SystemRoot\System32;$env:SystemRoot"
|
|
69
|
+
$env:PGROOT = $null
|
|
70
|
+
$env:PGDATA = $null
|
|
71
|
+
|
|
72
|
+
try {
|
|
73
|
+
& "$PgBin\postgres.exe" --version
|
|
74
|
+
if ($LASTEXITCODE -ne 0) { throw "FAIL: postgres --version" }
|
|
75
|
+
& "$PgBin\initdb.exe" -D $Data --username=postgres --auth-local=trust --no-locale -E UTF8 | Out-Null
|
|
76
|
+
if ($LASTEXITCODE -ne 0) { throw "FAIL: initdb" }
|
|
77
|
+
& "$PgBin\pg_ctl.exe" -D $Data -o "-p $Port -h 127.0.0.1" -l $Log -w start
|
|
78
|
+
if ($LASTEXITCODE -ne 0) { Get-Content $Log | Select-Object -Last 30; throw "FAIL: pg_ctl start" }
|
|
79
|
+
|
|
80
|
+
try {
|
|
81
|
+
& "$PgBin\psql.exe" -h 127.0.0.1 -p $Port -U postgres -d postgres -c "CREATE EXTENSION vector;" | Out-Null
|
|
82
|
+
if ($LASTEXITCODE -ne 0) { throw "FAIL: CREATE EXTENSION" }
|
|
83
|
+
$ExtV = & "$PgBin\psql.exe" -h 127.0.0.1 -p $Port -U postgres -d postgres -tAc "SELECT extversion FROM pg_extension WHERE extname='vector';"
|
|
84
|
+
if ($ExtV.Trim() -ne $PgvectorVer) { throw "FAIL: extversion='$ExtV' expected='$PgvectorVer'" }
|
|
85
|
+
Write-Host " PASS: fresh-extract boot + vector extension"
|
|
86
|
+
} finally {
|
|
87
|
+
& "$PgBin\pg_ctl.exe" -D $Data -m fast stop 2>$null | Out-Null
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
Write-Host "==> Test 5: second initdb on initialized dir refused"
|
|
91
|
+
$rc = (Start-Process -FilePath "$PgBin\initdb.exe" -ArgumentList "-D",$Data,"-U","postgres" -Wait -PassThru -NoNewWindow).ExitCode
|
|
92
|
+
if ($rc -eq 0) {
|
|
93
|
+
Write-Host " WARN: second initdb succeeded (unexpected)"
|
|
94
|
+
} else {
|
|
95
|
+
Write-Host " PASS: second initdb refused (exit $rc)"
|
|
96
|
+
}
|
|
97
|
+
} finally {
|
|
98
|
+
$env:PATH = $SavedPath
|
|
99
|
+
$env:PGROOT = $SavedPgRoot
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
Write-Host "==> All negative-path smokes: PASS"
|
|
103
|
+
}
|
|
104
|
+
finally {
|
|
105
|
+
Remove-Item -Recurse -Force $Work -ErrorAction SilentlyContinue
|
|
106
|
+
}
|
|
@@ -88,4 +88,4 @@ for (const target of ['heavy-worker', 'worker']) {
|
|
|
88
88
|
console.log(` single-call batches: ${batch1}/${batchTot} (${pct(batch1, batchTot)}%)`);
|
|
89
89
|
console.log(` content greps with -C/-A/-B: ${grepContentWithCtx}/${grepContent} (${pct(grepContentWithCtx, grepContent)}%) grep->read follow-ups: ${grepThenRead}`);
|
|
90
90
|
console.log(` same-path re-reads >=3x: ${rereads}`);
|
|
91
|
-
}
|
|
91
|
+
}
|