mirror of
https://github.com/dtzp555-max/ocp.git
synced 2026-07-26 23:45:08 +00:00
Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4759639d9b | ||
|
|
868a1953f4 |
@@ -52,6 +52,22 @@ Runtime: Node.js (ESM, `.mjs` throughout). No build step. No bundler. `server.mj
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## Testing: reaching faults inside `server.mjs`
|
||||||
|
|
||||||
|
`test-features.mjs` cannot `import` `server.mjs` (it calls `server.listen()` at top level), and that has twice led to the wrong conclusion that a class of bug is untestable. It isn't. Read this before writing "no regression test is possible here".
|
||||||
|
|
||||||
|
**There is a real live-server fixture.** `ltBoot(env, dir, nodeArgs)` (around `test-features.mjs:990`) spawns the actual `server.mjs` as a child with a **fake `claude` binary**, so integration tests cost no quota. `ltPost` / `ltPostStatus` / `ltWait` / `ltFreePort` round it out. It already covers boot gates, cache-epoch invalidation across two boots sharing one SQLite store, and system-prompt capture.
|
||||||
|
|
||||||
|
**`--stack-size` is a fault lever.** `ltBoot`'s third argument passes V8 flags to the child, which puts recursion- and argument-count-limited failures in reach at a much smaller input. `#193` needed a *synchronous throw* deep inside `spawnClaudeProcess`; `buildCliArgs` does `args.push("--allowedTools", ...ALLOWED_TOOLS)`, and under `--stack-size=200` that spread throws at ~24k elements instead of ~124k — which is what brings the trigger under Linux's `MAX_ARG_STRLEN` (131072 bytes for a single env string) so the test runs in CI rather than skipping. **No production fault hook was needed.**
|
||||||
|
|
||||||
|
Three rules that made it hold up, all learned the hard way:
|
||||||
|
|
||||||
|
- **Discover the threshold in a child under the same flags**, never in the test process — the parent's stack is not the one that matters.
|
||||||
|
- **Assert that the fault actually fired**, not just that the outcome looks right. `#193` asserts HTTP 500 *and* that the body carries `call stack size exceeded`; a control mutation (trigger neutered, bug still present) proves the test fails rather than passing vacuously.
|
||||||
|
- **Wait for the thing you are about to assert**, not a proxy for it. Waiting on `listening on` and then asserting a different line is a race (`#199`); waiting for the process to *exit* and then reading its `stderr` is another, because a terminated child's pipes may still hold unread data (`#203` — wait for the stdio to close, not for the exit).
|
||||||
|
|
||||||
|
Allocate ports with `ltFreePort()`. Fixed ports have caused at least one flake here.
|
||||||
|
|
||||||
## Release protocol
|
## Release protocol
|
||||||
|
|
||||||
OCP follows the machine-readable `release_kit:` overlay in `CLAUDE.md` (Iron Rule 5.5). Before any version bump or tag push, re-read that YAML block and walk every item in `new_feature_doc_expectations` and `bootstrap_quirk_policy`. Tag push triggers `.github/workflows/release.yml`, which creates the GitHub Release automatically — do not create the release manually.
|
OCP follows the machine-readable `release_kit:` overlay in `CLAUDE.md` (Iron Rule 5.5). Before any version bump or tag push, re-read that YAML block and walk every item in `new_feature_doc_expectations` and `bootstrap_quirk_policy`. Tag push triggers `.github/workflows/release.yml`, which creates the GitHub Release automatically — do not create the release manually.
|
||||||
|
|||||||
+12
@@ -2923,6 +2923,16 @@ async function handleChatCompletions(req, res) {
|
|||||||
const t0s = Date.now();
|
const t0s = Date.now();
|
||||||
const promptCharsS = messages.reduce((a, m) => a + contentToText(m.content).length, 0);
|
const promptCharsS = messages.reduce((a, m) => a + contentToText(m.content).length, 0);
|
||||||
let structuredHash = null;
|
let structuredHash = null;
|
||||||
|
// DO NOT collapse this with `dedupKey` below (#200). The two cacheHash calls take IDENTICAL
|
||||||
|
// arguments and look like obvious duplicate work — they are not interchangeable, because
|
||||||
|
// their GUARDS differ: this one additionally requires CACHE_TTL > 0. CLAUDE_CACHE_TTL
|
||||||
|
// DEFAULTS TO 0, so in the default configuration structuredHash is null while dedupKey must
|
||||||
|
// still be computed — it drives #153's single-flight stampede protection, which is
|
||||||
|
// deliberately independent of whether response caching is on. `dedupKey = structuredHash`
|
||||||
|
// would therefore silently disable stampede protection by default, in exactly the
|
||||||
|
// concurrent-AI-Task case it exists to bound. The duplicate call is the honest price of the
|
||||||
|
// asymmetry. If you do deduplicate it, compute once under the WEAKER guard and derive the
|
||||||
|
// cache lookup under the stronger one — and add a stampede test before you do.
|
||||||
if (CACHE_TTL > 0 && !conversationId && !hasCacheControl(messages)) {
|
if (CACHE_TTL > 0 && !conversationId && !hasCacheControl(messages)) {
|
||||||
structuredHash = cacheHash(cacheModel, messages, { keyId: req._authKeyId, temperature: parsed.temperature, max_tokens: parsed.max_tokens, top_p: parsed.top_p, structured, configEpoch: CONFIG_EPOCH });
|
structuredHash = cacheHash(cacheModel, messages, { keyId: req._authKeyId, temperature: parsed.temperature, max_tokens: parsed.max_tokens, top_p: parsed.top_p, structured, configEpoch: CONFIG_EPOCH });
|
||||||
try {
|
try {
|
||||||
@@ -2942,6 +2952,8 @@ async function handleChatCompletions(req, res) {
|
|||||||
// firing several AI Tasks at once) must NOT each pay N× — they share one flight. We dedup every
|
// firing several AI Tasks at once) must NOT each pay N× — they share one flight. We dedup every
|
||||||
// one-off structured request (not stateful sessions / client-side prompt caching), independent of
|
// one-off structured request (not stateful sessions / client-side prompt caching), independent of
|
||||||
// whether OCP response caching is enabled; when caching IS on, the same key gates cache read/write.
|
// whether OCP response caching is enabled; when caching IS on, the same key gates cache read/write.
|
||||||
|
// Note the guard here is deliberately WEAKER than structuredHash's — no CACHE_TTL check. See the
|
||||||
|
// do-not-collapse comment above (#200).
|
||||||
const dedupKey = (!conversationId && !hasCacheControl(messages))
|
const dedupKey = (!conversationId && !hasCacheControl(messages))
|
||||||
? cacheHash(cacheModel, messages, { keyId: req._authKeyId, temperature: parsed.temperature, max_tokens: parsed.max_tokens, top_p: parsed.top_p, structured, configEpoch: CONFIG_EPOCH })
|
? cacheHash(cacheModel, messages, { keyId: req._authKeyId, temperature: parsed.temperature, max_tokens: parsed.max_tokens, top_p: parsed.top_p, structured, configEpoch: CONFIG_EPOCH })
|
||||||
: null;
|
: null;
|
||||||
|
|||||||
Reference in New Issue
Block a user