feat: add database reset functionality and implement rulebook tests
This commit is contained in:
@@ -262,6 +262,7 @@ export function createDatabase(config: DatabaseConfig): CharacterGardenDatabase
|
||||
DELETE FROM world_states;
|
||||
DELETE FROM turns;
|
||||
DELETE FROM entities;
|
||||
DELETE FROM rulebooks;
|
||||
`);
|
||||
},
|
||||
|
||||
|
||||
6
charactergarden/app/src/tests/sqliteProbe.ts
Normal file
6
charactergarden/app/src/tests/sqliteProbe.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import Database from "better-sqlite3";
|
||||
|
||||
const db = new Database(process.env.DB_PATH ?? "/var/lib/charactergarden/app.db");
|
||||
db.prepare("CREATE TABLE IF NOT EXISTS probe(value TEXT)").run();
|
||||
db.close();
|
||||
console.log("sqlite_probe_ok");
|
||||
34
charactergarden/app/src/tests/verifyDefaultSeed.ts
Normal file
34
charactergarden/app/src/tests/verifyDefaultSeed.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
import { createCharacterGardenApp } from "../app";
|
||||
|
||||
function run(): void {
|
||||
const app = createCharacterGardenApp(process.env.DB_PATH ?? "/var/lib/charactergarden/app.db");
|
||||
const snapshot = app.getSnapshot();
|
||||
const rulebook = app.getRulebook();
|
||||
|
||||
assert.equal(snapshot.turns.length, 0, "Expected no persisted turns after database reset.");
|
||||
assert.ok(snapshot.worldState.entities.player, "Expected player in default world state.");
|
||||
assert.ok(snapshot.worldState.entities.room_start, "Expected room_start in default world state.");
|
||||
assert.equal(rulebook.id, "rulebook_default", "Expected default rulebook to be active.");
|
||||
assert.equal(rulebook.version, 1, "Expected default rulebook version to be 1.");
|
||||
|
||||
console.log(
|
||||
JSON.stringify(
|
||||
{
|
||||
ok: true,
|
||||
entityCount: Object.keys(snapshot.worldState.entities).length,
|
||||
turnCount: snapshot.turns.length,
|
||||
rulebookId: rulebook.id,
|
||||
rulebookVersion: rulebook.version,
|
||||
ruleCount: rulebook.rules.length,
|
||||
},
|
||||
null,
|
||||
2
|
||||
)
|
||||
);
|
||||
|
||||
app.db.close();
|
||||
}
|
||||
|
||||
run();
|
||||
0
charactergarden/data/sqlite/test_write_probe.txt
Normal file
0
charactergarden/data/sqlite/test_write_probe.txt
Normal file
@@ -7,10 +7,10 @@ services:
|
||||
environment:
|
||||
- APP_PORT=3000
|
||||
- NODE_ENV=${NODE_ENV:-development}
|
||||
- DB_PATH=/data/sqlite/app.db
|
||||
- DB_PATH=/var/lib/charactergarden/app.db
|
||||
- OLLAMA_URL=${OLLAMA_URL:-http://ollama:11434}
|
||||
volumes:
|
||||
- ./data:/data
|
||||
- sqlite_data:/var/lib/charactergarden
|
||||
- ./app/src:/app/src
|
||||
|
||||
frontend:
|
||||
@@ -38,3 +38,4 @@ services:
|
||||
|
||||
volumes:
|
||||
ollama_data:
|
||||
sqlite_data:
|
||||
|
||||
@@ -33,6 +33,11 @@ type Turn = {
|
||||
minConfidence: number;
|
||||
status: "resolved" | "needs_clarification" | "rejected";
|
||||
selectedConfidence?: number;
|
||||
candidates?: Array<{
|
||||
action: Action;
|
||||
confidence: number;
|
||||
rationale?: string;
|
||||
}>;
|
||||
diagnostics: string[];
|
||||
clarification?: {
|
||||
reasonCode: string;
|
||||
@@ -70,6 +75,11 @@ type ProcessTurnResponse = {
|
||||
minConfidence: number;
|
||||
status: "resolved" | "needs_clarification" | "rejected";
|
||||
selectedConfidence?: number;
|
||||
candidates: Array<{
|
||||
action: Action;
|
||||
confidence: number;
|
||||
rationale?: string;
|
||||
}>;
|
||||
diagnostics: string[];
|
||||
clarification?: {
|
||||
reasonCode: string;
|
||||
@@ -109,6 +119,13 @@ type SceneRulebook = {
|
||||
updatedAt: number;
|
||||
};
|
||||
|
||||
type RulebookListItem = {
|
||||
id: string;
|
||||
name: string;
|
||||
version: number;
|
||||
updatedAt: number;
|
||||
};
|
||||
|
||||
const starterPrompts = [
|
||||
"look around",
|
||||
"take key",
|
||||
@@ -145,7 +162,7 @@ async function fetchJson<T>(input: RequestInfo, init?: RequestInit): Promise<T>
|
||||
// Rulebook editor component
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function RulebookEditor() {
|
||||
function RulebookEditor(props: { onSaved?: (rulebook: SceneRulebook) => void }) {
|
||||
const [rulebook, setRulebook] = useState<SceneRulebook | null>(null);
|
||||
const [drafts, setDrafts] = useState<Record<string, string>>({});
|
||||
const [parseErrors, setParseErrors] = useState<Record<string, string>>({});
|
||||
@@ -216,6 +233,7 @@ function RulebookEditor() {
|
||||
body: JSON.stringify({ ...rulebook, rules: updatedRules }),
|
||||
});
|
||||
setRulebook(updated);
|
||||
props.onSaved?.(updated);
|
||||
setSaved(true);
|
||||
setTimeout(() => setSaved(false), 2500);
|
||||
} catch (err) {
|
||||
@@ -327,10 +345,21 @@ export default function App() {
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [tab, setTab] = useState<"world" | "rulebook">("world");
|
||||
const [activeRulebook, setActiveRulebook] = useState<SceneRulebook | null>(null);
|
||||
const [rulebooks, setRulebooks] = useState<RulebookListItem[]>([]);
|
||||
|
||||
async function refreshRulebookState() {
|
||||
const [active, list] = await Promise.all([
|
||||
fetchJson<SceneRulebook>("/api/rulebook"),
|
||||
fetchJson<RulebookListItem[]>("/api/rulebooks"),
|
||||
]);
|
||||
setActiveRulebook(active);
|
||||
setRulebooks(list);
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
void fetchJson<AppSnapshot>("/api/state")
|
||||
.then((data) => {
|
||||
void Promise.all([fetchJson<AppSnapshot>("/api/state"), refreshRulebookState()])
|
||||
.then(([data]) => {
|
||||
setSnapshot(data);
|
||||
setLoading(false);
|
||||
})
|
||||
@@ -352,7 +381,10 @@ export default function App() {
|
||||
body: JSON.stringify({ input }),
|
||||
});
|
||||
setLatest(result);
|
||||
const nextSnapshot = await fetchJson<AppSnapshot>("/api/state");
|
||||
const [nextSnapshot] = await Promise.all([
|
||||
fetchJson<AppSnapshot>("/api/state"),
|
||||
refreshRulebookState(),
|
||||
]);
|
||||
setSnapshot(nextSnapshot);
|
||||
} catch (submitError) {
|
||||
setError(submitError instanceof Error ? submitError.message : "Unknown error");
|
||||
@@ -371,6 +403,7 @@ export default function App() {
|
||||
});
|
||||
setSnapshot(result);
|
||||
setLatest(null);
|
||||
await refreshRulebookState();
|
||||
} catch (resetError) {
|
||||
setError(resetError instanceof Error ? resetError.message : "Unknown error");
|
||||
}
|
||||
@@ -475,6 +508,25 @@ export default function App() {
|
||||
<strong>Diagnostics:</strong> {latest.interpreter.diagnostics.join(" | ")}
|
||||
</p>
|
||||
) : null}
|
||||
{latest.actions.length > 0 ? (
|
||||
<details>
|
||||
<summary>Selected actions ({latest.actions.length})</summary>
|
||||
<pre>{JSON.stringify(latest.actions, null, 2)}</pre>
|
||||
</details>
|
||||
) : null}
|
||||
{latest.interpreter.candidates.length > 0 ? (
|
||||
<details>
|
||||
<summary>Interpreter candidates ({latest.interpreter.candidates.length})</summary>
|
||||
<ul className="timeline-list compact">
|
||||
{latest.interpreter.candidates.map((candidate, index) => (
|
||||
<li key={`${candidate.action.type}-${index}`}>
|
||||
{candidate.action.type} at {formatConfidence(candidate.confidence)}
|
||||
{candidate.rationale ? ` - ${candidate.rationale}` : ""}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</details>
|
||||
) : null}
|
||||
</section>
|
||||
) : null}
|
||||
|
||||
@@ -504,6 +556,14 @@ export default function App() {
|
||||
<article className="panel">
|
||||
<h2>World state</h2>
|
||||
{loading && !snapshot ? <p>Loading...</p> : null}
|
||||
{snapshot ? (
|
||||
<div className="meta-grid">
|
||||
<p className="meta-kv"><strong>World ID:</strong> {snapshot.worldState.id}</p>
|
||||
<p className="meta-kv"><strong>Created:</strong> {formatTurnTime(snapshot.worldState.createdAt)}</p>
|
||||
<p className="meta-kv"><strong>Domain:</strong> {String(snapshot.worldState.metadata?.domain ?? "unknown")}</p>
|
||||
<p className="meta-kv"><strong>World schema:</strong> {String(snapshot.worldState.metadata?.version ?? "unknown")}</p>
|
||||
</div>
|
||||
) : null}
|
||||
<ul className="entity-list">
|
||||
{entities.map((entity) => (
|
||||
<li key={entity.id}>
|
||||
@@ -544,11 +604,35 @@ export default function App() {
|
||||
))}
|
||||
</ul>
|
||||
</article>
|
||||
|
||||
<article className="panel">
|
||||
<h2>System</h2>
|
||||
{activeRulebook ? (
|
||||
<div className="meta-grid">
|
||||
<p className="meta-kv"><strong>Active rulebook:</strong> {activeRulebook.name}</p>
|
||||
<p className="meta-kv"><strong>Rulebook ID:</strong> {activeRulebook.id}</p>
|
||||
<p className="meta-kv"><strong>Rulebook version:</strong> {activeRulebook.version}</p>
|
||||
<p className="meta-kv"><strong>Updated:</strong> {formatTurnTime(activeRulebook.updatedAt)}</p>
|
||||
<p className="meta-kv"><strong>Saved rulebooks:</strong> {rulebooks.length}</p>
|
||||
</div>
|
||||
) : (
|
||||
<p>Loading rulebook info...</p>
|
||||
)}
|
||||
{rulebooks.length > 0 ? (
|
||||
<ul className="timeline-list compact">
|
||||
{rulebooks.map((rb) => (
|
||||
<li key={rb.id}>
|
||||
<strong>{rb.name}</strong> ({rb.id}) v{rb.version} - updated {formatTurnTime(rb.updatedAt)}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
) : null}
|
||||
</article>
|
||||
</>
|
||||
) : (
|
||||
<article className="panel panel--full">
|
||||
<h2>Rulebook editor</h2>
|
||||
<RulebookEditor />
|
||||
<RulebookEditor onSaved={(rulebook) => setActiveRulebook(rulebook)} />
|
||||
</article>
|
||||
)}
|
||||
</section>
|
||||
|
||||
@@ -161,6 +161,19 @@ pre {
|
||||
padding: 8px 10px;
|
||||
}
|
||||
|
||||
.meta-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 6px 12px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.meta-kv {
|
||||
margin: 0;
|
||||
font-size: 0.86rem;
|
||||
color: rgba(244, 239, 228, 0.86);
|
||||
}
|
||||
|
||||
.error-banner {
|
||||
margin-top: 16px;
|
||||
color: #ffd2b8;
|
||||
@@ -375,6 +388,10 @@ pre {
|
||||
.panel {
|
||||
padding: 14px;
|
||||
}
|
||||
|
||||
.meta-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
.turn-time {
|
||||
|
||||
Reference in New Issue
Block a user