oh-my-opencode 5.0.0-beta.11 → 5.0.0-beta.13
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/.agents/skills/senpi-qa/SKILL.md +80 -0
- package/.agents/skills/senpi-qa/scripts/resolve-evidence-dir.mjs +65 -0
- package/.agents/skills/senpi-qa/scripts/resolve-evidence-dir.test.mjs +171 -0
- package/dist/cli/index.js +6784 -4321
- package/dist/cli-node/index.js +6784 -4321
- package/dist/create-hooks.d.ts +1 -0
- package/dist/features/btw-side/btw-command-draft.d.ts +7 -0
- package/dist/features/btw-side/context-injector.d.ts +15 -0
- package/dist/features/btw-side/index.d.ts +8 -0
- package/dist/features/btw-side/metadata.d.ts +15 -0
- package/dist/features/btw-side/parent-context-budget.d.ts +9 -0
- package/dist/features/btw-side/server-session-registry.d.ts +8 -0
- package/dist/features/btw-side/tui-adoption-cache.d.ts +13 -0
- package/dist/features/btw-side/tui-adoption-guard.d.ts +5 -0
- package/dist/features/btw-side/tui-controller-types.d.ts +63 -0
- package/dist/features/btw-side/tui-controller.d.ts +14 -0
- package/dist/features/btw-side/tui-keymap.d.ts +10 -0
- package/dist/features/btw-side/tui-parent-validator.d.ts +6 -0
- package/dist/features/btw-side/tui-prompt-queue.d.ts +8 -0
- package/dist/features/btw-side/tui-session-bridge.d.ts +11 -0
- package/dist/features/btw-side/tui-side-removal.d.ts +11 -0
- package/dist/features/btw-side/tui-side-start.d.ts +9 -0
- package/dist/features/btw-side/tui-wiring.d.ts +8 -0
- package/dist/hooks/runtime-fallback/chat-message-handler.d.ts +2 -0
- package/dist/hooks/runtime-fallback/retry-model-payload.d.ts +1 -0
- package/dist/hooks/runtime-fallback/runtime-model-settings.d.ts +6 -0
- package/dist/hooks/runtime-fallback/session-status-handler.d.ts +1 -1
- package/dist/hooks/runtime-fallback/types.d.ts +3 -1
- package/dist/index.js +6512 -3851
- package/dist/plugin/event-session-lifecycle.d.ts +1 -0
- package/dist/plugin/hooks/create-core-hooks.d.ts +1 -0
- package/dist/plugin/hooks/create-transform-hooks.d.ts +2 -0
- package/dist/plugin/messages-transform.d.ts +1 -0
- package/dist/shared/index.d.ts +1 -0
- package/dist/shared/runtime-fallback-retry-marker.d.ts +16 -0
- package/dist/tui.js +8102 -6793
- package/package.json +24 -23
- package/packages/git-bash-mcp/dist/cli.js +5 -1
- package/packages/lsp-daemon/dist/cli.js +5 -1
- package/packages/lsp-daemon/dist/client.js +5 -1
- package/packages/lsp-daemon/dist/index.js +5 -1
- package/packages/lsp-tools-mcp/dist/cli.js +5 -1
- package/packages/lsp-tools-mcp/dist/mcp.js +5 -1
- package/packages/omo-codex/plugin/.codex-plugin/plugin.json +1 -1
- package/packages/omo-codex/plugin/components/bootstrap/hooks/hooks.json +1 -1
- package/packages/omo-codex/plugin/components/bootstrap/package.json +1 -1
- package/packages/omo-codex/plugin/components/codegraph/dist/cli.js +14 -1
- package/packages/omo-codex/plugin/components/codegraph/dist/serve.js +14 -1
- package/packages/omo-codex/plugin/components/codegraph/package.json +1 -1
- package/packages/omo-codex/plugin/components/comment-checker/hooks/hooks.json +1 -1
- package/packages/omo-codex/plugin/components/comment-checker/package.json +1 -1
- package/packages/omo-codex/plugin/components/git-bash/hooks/hooks.json +2 -2
- package/packages/omo-codex/plugin/components/git-bash/package.json +1 -1
- package/packages/omo-codex/plugin/components/lazycodex-executor-verify/hooks/hooks.json +1 -1
- package/packages/omo-codex/plugin/components/lazycodex-executor-verify/package.json +1 -1
- package/packages/omo-codex/plugin/components/lsp/dist/.omo-runtime-manifest.json +2 -2
- package/packages/omo-codex/plugin/components/lsp/hooks/hooks.json +2 -2
- package/packages/omo-codex/plugin/components/lsp/package.json +1 -1
- package/packages/omo-codex/plugin/components/rules/hooks/hooks.json +4 -4
- package/packages/omo-codex/plugin/components/rules/package.json +1 -1
- package/packages/omo-codex/plugin/components/start-work-continuation/hooks/hooks.json +2 -2
- package/packages/omo-codex/plugin/components/start-work-continuation/package.json +1 -1
- package/packages/omo-codex/plugin/components/teammode/hooks/hooks.json +1 -1
- package/packages/omo-codex/plugin/components/teammode/package.json +1 -1
- package/packages/omo-codex/plugin/components/telemetry/dist/cli.js +2242 -381
- package/packages/omo-codex/plugin/components/telemetry/dist/posthog.js +2247 -386
- package/packages/omo-codex/plugin/components/telemetry/hooks/hooks.json +1 -1
- package/packages/omo-codex/plugin/components/telemetry/package.json +1 -1
- package/packages/omo-codex/plugin/components/ultrawork/hooks/hooks.json +1 -1
- package/packages/omo-codex/plugin/components/ultrawork/package.json +1 -1
- package/packages/omo-codex/plugin/components/ulw-loop/hooks/hooks.json +4 -4
- package/packages/omo-codex/plugin/components/ulw-loop/package.json +1 -1
- package/packages/omo-codex/plugin/hooks/post-compact-resetting-git-bash-mcp-reminder.json +1 -1
- package/packages/omo-codex/plugin/hooks/post-compact-resetting-lsp-diagnostics-cache.json +1 -1
- package/packages/omo-codex/plugin/hooks/post-compact-resetting-project-rule-cache.json +1 -1
- package/packages/omo-codex/plugin/hooks/post-tool-use-checking-codegraph-init-guidance.json +1 -1
- package/packages/omo-codex/plugin/hooks/post-tool-use-checking-comments.json +1 -1
- package/packages/omo-codex/plugin/hooks/post-tool-use-checking-lsp-diagnostics.json +1 -1
- package/packages/omo-codex/plugin/hooks/post-tool-use-checking-thread-title-hygiene.json +1 -1
- package/packages/omo-codex/plugin/hooks/post-tool-use-matching-project-rules.json +1 -1
- package/packages/omo-codex/plugin/hooks/pre-tool-use-enforcing-unlimited-goal-budget.json +1 -1
- package/packages/omo-codex/plugin/hooks/pre-tool-use-guarding-ulw-loop-spawns.json +1 -1
- package/packages/omo-codex/plugin/hooks/pre-tool-use-recommending-git-bash-mcp.json +1 -1
- package/packages/omo-codex/plugin/hooks/session-start-checking-auto-update.json +1 -1
- package/packages/omo-codex/plugin/hooks/session-start-checking-bootstrap-provisioning.json +1 -1
- package/packages/omo-codex/plugin/hooks/session-start-checking-codegraph-bootstrap.json +1 -1
- package/packages/omo-codex/plugin/hooks/session-start-loading-project-rules.json +1 -1
- package/packages/omo-codex/plugin/hooks/session-start-recording-session-telemetry.json +1 -1
- package/packages/omo-codex/plugin/hooks/stop-checking-start-work-continuation.json +1 -1
- package/packages/omo-codex/plugin/hooks/stop-checking-ulw-loop-resume.json +1 -1
- package/packages/omo-codex/plugin/hooks/subagent-stop-checking-start-work-continuation.json +1 -1
- package/packages/omo-codex/plugin/hooks/subagent-stop-verifying-lazycodex-executor-evidence.json +1 -1
- package/packages/omo-codex/plugin/hooks/user-prompt-submit-checking-ultrawork-trigger.json +1 -1
- package/packages/omo-codex/plugin/hooks/user-prompt-submit-checking-ulw-loop-steering.json +1 -1
- package/packages/omo-codex/plugin/hooks/user-prompt-submit-loading-project-rules.json +1 -1
- package/packages/omo-codex/plugin/package-lock.json +13 -13
- package/packages/omo-codex/plugin/package.json +1 -1
- package/packages/omo-codex/scripts/install-dist/install-local.mjs +2307 -401
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: senpi-qa
|
|
3
|
+
description: "QA the omo Senpi adapter (packages/omo-senpi, packages/senpi-task) against the REAL senpi binary in strict isolation, and write every artifact to the one canonical evidence path .omo/evidence/omo-senpi-adapter/<slug>/. The live drivers under packages/omo-senpi/scripts/qa/ create their own isolated SENPI_CODING_AGENT_DIR and ignore the caller's, so the real ~/.senpi/agent is never written. Ships scripts/resolve-evidence-dir.mjs, which is the ONLY sanctioned way to pick an evidence directory: it rejects traversal, separators, absolute paths, and stray roots such as local-ignore/qa-evidence. Use whenever someone changes anything under packages/omo-senpi or packages/senpi-task, or wants to QA, smoke-test, verify, or debug the Senpi adapter, the task/team engine, the DAG, task RPC, or skill delivery. Triggers: senpi qa, qa senpi, senpi-qa, test senpi adapter, verify senpi task, senpi task e2e, senpi team e2e, task dag qa, live senpi driver, senpi evidence path."
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Senpi QA
|
|
7
|
+
|
|
8
|
+
QA the omo Senpi adapter (`packages/omo-senpi/`) and the task engine
|
|
9
|
+
(`packages/senpi-task/`) by driving the REAL `senpi` binary. Unit tests never
|
|
10
|
+
count as live QA here: `bun run test:senpi` is the package gate, the drivers in
|
|
11
|
+
`packages/omo-senpi/scripts/qa/` are the harness proof.
|
|
12
|
+
|
|
13
|
+
## Golden rules
|
|
14
|
+
|
|
15
|
+
- **Evidence lives at exactly one path.** Every artifact goes under
|
|
16
|
+
`.omo/evidence/omo-senpi-adapter/<slug>/`. Pick it with
|
|
17
|
+
`scripts/resolve-evidence-dir.mjs` and nothing else — a hand-typed path is how
|
|
18
|
+
runs end up somewhere like `local-ignore/qa-evidence/`, which no reviewer reads
|
|
19
|
+
and the PR cannot cite.
|
|
20
|
+
- **The real agent dir stays untouched.** The live drivers build their own
|
|
21
|
+
isolated `SENPI_CODING_AGENT_DIR` and deliberately IGNORE a caller-provided
|
|
22
|
+
one, so `~/.senpi/agent` is never used as the sandbox. Report the driver's
|
|
23
|
+
`realSenpiUntouched` / changed-path fields and the isolated agent-dir path;
|
|
24
|
+
treat a whole-directory digest as supporting evidence, not proof by itself.
|
|
25
|
+
- **No binary means SKIP, not silence.** When `senpi` is absent the live drivers
|
|
26
|
+
report `SKIP` or `FAIL` in their final JSON rather than degrading to the real
|
|
27
|
+
home. A `SKIP` is not a pass — say so in the evidence README.
|
|
28
|
+
- **The captured JSON is the evidence.** No file on disk means the QA did not
|
|
29
|
+
happen, which means no commit and no push.
|
|
30
|
+
|
|
31
|
+
## Resolve the evidence directory first
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
ev="$(node .agents/skills/senpi-qa/scripts/resolve-evidence-dir.mjs \
|
|
35
|
+
--repo-root "$(git rev-parse --show-toplevel)" --slug <YYYYMMDD>-<short-slug>)"
|
|
36
|
+
mkdir -p "$ev"
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
The resolver returns an absolute path and creates nothing, so the caller decides
|
|
40
|
+
when the directory appears. A slug is ONE relative segment of lowercase letters,
|
|
41
|
+
digits, and hyphens (`20260820-senpi-qa-contract`). Separators, `.`/`..`,
|
|
42
|
+
traversal, absolute paths, and a non-git root are rejected with a non-zero exit
|
|
43
|
+
and a message naming the offending slug.
|
|
44
|
+
|
|
45
|
+
## Router: pick your case
|
|
46
|
+
|
|
47
|
+
| You changed… | Run | Proves |
|
|
48
|
+
|---|---|---|
|
|
49
|
+
| Any adapter code, as the fast precondition | `node packages/omo-senpi/scripts/qa/drive.mjs --self-test` | the driver + isolation harness itself works |
|
|
50
|
+
| Adapter wiring reaching a live session | `node packages/omo-senpi/scripts/qa/drive.mjs` | a real senpi run with the plugin loaded, isolated agent dir, and no attributed real-home changes |
|
|
51
|
+
| Task lifecycle (single + batch) | `SENPI_BIN="$(command -v senpi)" node packages/omo-senpi/scripts/qa/task-e2e.mjs` | live task start/stream/terminal states |
|
|
52
|
+
| Team delivery, shutdown, reclaim, restart recovery | `SENPI_BIN="$(command -v senpi)" node packages/omo-senpi/scripts/qa/team-e2e.mjs` | injection delivery and exactly-once recovery |
|
|
53
|
+
| Task RPC driver scripts | `node packages/omo-senpi/scripts/qa/task-rpc-e2e.mjs --self-test` | the RPC surface contract |
|
|
54
|
+
| Skill delivery into a task | `SENPI_BIN="$(command -v senpi)" node packages/omo-senpi/scripts/qa/task-load-skills-e2e.mjs` | skills reach the child |
|
|
55
|
+
| Continuation behavior | `node packages/omo-senpi/scripts/qa/probe-continuation.mjs` | turns continue as expected |
|
|
56
|
+
| DAG state machine / runners | `bun test packages/senpi-task` | unit + chaos invariants (NOT live proof) |
|
|
57
|
+
|
|
58
|
+
Point a driver's output at the resolved directory, e.g.:
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
TASK_E2E_OUT_DIR="$ev/live-task-dag" SENPI_BIN="$(command -v senpi)" \
|
|
62
|
+
node packages/omo-senpi/scripts/qa/task-e2e.mjs
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## Package gate
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
tsgo --noEmit -p packages/omo-senpi/tsconfig.json
|
|
69
|
+
bun run test:senpi
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## Write the evidence README
|
|
73
|
+
|
|
74
|
+
Every run leaves `$ev/README.md` a reviewer can read without rerunning anything.
|
|
75
|
+
The required sections are the repo-wide evidence rules in the root
|
|
76
|
+
[`AGENTS.md`](../../../AGENTS.md) (what was tested / observed / why it is enough /
|
|
77
|
+
what was omitted). For Senpi, record the driver's changed-path/isolation fields
|
|
78
|
+
and sandbox agent-dir path. Some drivers report sandbox paths without removing
|
|
79
|
+
them; the caller must delete every task-owned sandbox and verify child PIDs are
|
|
80
|
+
terminal before writing the cleanup receipt.
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// resolve-evidence-dir.mjs - pick the canonical evidence directory for a live Senpi QA run.
|
|
3
|
+
//
|
|
4
|
+
// Live Senpi QA artifacts belong under .omo/evidence/omo-senpi-adapter/<slug>/ and nowhere else.
|
|
5
|
+
// That path is the repository-standard location every reviewer and AGENTS.md rule points at, so a
|
|
6
|
+
// hand-typed alternative (local-ignore/qa-evidence/..., a temp dir, a traversal) silently strands
|
|
7
|
+
// the evidence where the PR cannot cite it. Resolving through this script makes the path a checked
|
|
8
|
+
// contract instead of a convention.
|
|
9
|
+
//
|
|
10
|
+
// The resolver COMPUTES and VALIDATES only; creating the directory stays the caller's decision, so
|
|
11
|
+
// a rejected slug never leaves a stray directory behind.
|
|
12
|
+
//
|
|
13
|
+
// node resolve-evidence-dir.mjs --repo-root "$(git rev-parse --show-toplevel)" --slug 20260820-my-run
|
|
14
|
+
//
|
|
15
|
+
// Prints the absolute path to stdout. Exit 0 iff the root is a git worktree and the slug is safe.
|
|
16
|
+
import { existsSync, realpathSync } from "node:fs"
|
|
17
|
+
import { join, resolve } from "node:path"
|
|
18
|
+
import { fileURLToPath } from "node:url"
|
|
19
|
+
|
|
20
|
+
export const EVIDENCE_RELATIVE_ROOT = join(".omo", "evidence", "omo-senpi-adapter")
|
|
21
|
+
|
|
22
|
+
// One relative segment: lowercase alphanumerics joined by single hyphens. This is what makes the
|
|
23
|
+
// path predictable - it also rejects every separator, traversal, and absolute form outright, since
|
|
24
|
+
// none of them can match.
|
|
25
|
+
const SAFE_SLUG = /^[a-z0-9]+(?:-[a-z0-9]+)*$/
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* @param {{ repoRoot: string, slug: string }} input
|
|
29
|
+
* @returns {string} absolute evidence directory; NOT created
|
|
30
|
+
*/
|
|
31
|
+
export function resolveEvidenceDir({ repoRoot, slug }) {
|
|
32
|
+
if (typeof repoRoot !== "string" || repoRoot.length === 0) {
|
|
33
|
+
throw new Error("repoRoot must be a non-empty path to a git worktree")
|
|
34
|
+
}
|
|
35
|
+
if (!existsSync(join(repoRoot, ".git"))) {
|
|
36
|
+
throw new Error(`repoRoot is not a git worktree: ${repoRoot}`)
|
|
37
|
+
}
|
|
38
|
+
if (typeof slug !== "string" || !SAFE_SLUG.test(slug)) {
|
|
39
|
+
throw new Error(
|
|
40
|
+
`slug must be one lowercase alphanumeric-and-hyphen segment (got ${JSON.stringify(slug)}); ` +
|
|
41
|
+
`evidence lives under ${EVIDENCE_RELATIVE_ROOT}/<slug>`,
|
|
42
|
+
)
|
|
43
|
+
}
|
|
44
|
+
return resolve(repoRoot, EVIDENCE_RELATIVE_ROOT, slug)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const isDirectRun =
|
|
48
|
+
process.argv[1] !== undefined &&
|
|
49
|
+
existsSync(process.argv[1]) &&
|
|
50
|
+
realpathSync(process.argv[1]) === realpathSync(fileURLToPath(import.meta.url))
|
|
51
|
+
|
|
52
|
+
if (isDirectRun) {
|
|
53
|
+
const args = process.argv.slice(2)
|
|
54
|
+
const read = (flag) => {
|
|
55
|
+
const at = args.indexOf(flag)
|
|
56
|
+
return at === -1 ? undefined : args[at + 1]
|
|
57
|
+
}
|
|
58
|
+
try {
|
|
59
|
+
const dir = resolveEvidenceDir({ repoRoot: read("--repo-root") ?? process.cwd(), slug: read("--slug") })
|
|
60
|
+
process.stdout.write(`${dir}\n`)
|
|
61
|
+
} catch (error) {
|
|
62
|
+
process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`)
|
|
63
|
+
process.exit(1)
|
|
64
|
+
}
|
|
65
|
+
}
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import { afterEach, describe, expect, test } from "bun:test"
|
|
2
|
+
import { spawnSync } from "node:child_process"
|
|
3
|
+
import { existsSync, mkdirSync, mkdtempSync, rmSync, symlinkSync } from "node:fs"
|
|
4
|
+
import { tmpdir } from "node:os"
|
|
5
|
+
import { join, relative } from "node:path"
|
|
6
|
+
import { fileURLToPath, pathToFileURL } from "node:url"
|
|
7
|
+
|
|
8
|
+
import { EVIDENCE_RELATIVE_ROOT, resolveEvidenceDir } from "./resolve-evidence-dir.mjs"
|
|
9
|
+
|
|
10
|
+
const cleanupRoots = []
|
|
11
|
+
const cliPath = fileURLToPath(new URL("./resolve-evidence-dir.mjs", import.meta.url))
|
|
12
|
+
|
|
13
|
+
afterEach(() => {
|
|
14
|
+
for (const root of cleanupRoots.splice(0)) rmSync(root, { recursive: true, force: true })
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
function makeGitRoot() {
|
|
18
|
+
const root = mkdtempSync(join(tmpdir(), "senpi-qa-evidence-"))
|
|
19
|
+
cleanupRoots.push(root)
|
|
20
|
+
mkdirSync(join(root, ".git"))
|
|
21
|
+
return root
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
describe("resolveEvidenceDir", () => {
|
|
25
|
+
test("#given a git worktree and a safe slug #when resolved #then it returns the canonical adapter evidence directory", () => {
|
|
26
|
+
// given
|
|
27
|
+
const repoRoot = makeGitRoot()
|
|
28
|
+
|
|
29
|
+
// when
|
|
30
|
+
const resolved = resolveEvidenceDir({ repoRoot, slug: "20260820-senpi-qa-contract" })
|
|
31
|
+
|
|
32
|
+
// then
|
|
33
|
+
expect(resolved).toBe(join(repoRoot, ".omo", "evidence", "omo-senpi-adapter", "20260820-senpi-qa-contract"))
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
test("#given a relative git worktree path #when resolved #then it still returns an absolute evidence directory", () => {
|
|
37
|
+
// given
|
|
38
|
+
const repoRoot = makeGitRoot()
|
|
39
|
+
const relativeRoot = relative(process.cwd(), repoRoot)
|
|
40
|
+
|
|
41
|
+
// when
|
|
42
|
+
const resolved = resolveEvidenceDir({ repoRoot: relativeRoot, slug: "20260820-relative-root" })
|
|
43
|
+
|
|
44
|
+
// then
|
|
45
|
+
expect(resolved).toBe(join(repoRoot, ".omo", "evidence", "omo-senpi-adapter", "20260820-relative-root"))
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
test("#given a resolved evidence path #when the resolver returns #then it has created no directory", () => {
|
|
49
|
+
// given
|
|
50
|
+
const repoRoot = makeGitRoot()
|
|
51
|
+
|
|
52
|
+
// when
|
|
53
|
+
const resolved = resolveEvidenceDir({ repoRoot, slug: "20260820-senpi-qa-contract" })
|
|
54
|
+
|
|
55
|
+
// then
|
|
56
|
+
expect(existsSync(resolved)).toBe(false)
|
|
57
|
+
expect(existsSync(join(repoRoot, ".omo"))).toBe(false)
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
test("#given the adapter evidence root #when it is the declared relative root #then it stays under .omo/evidence", () => {
|
|
61
|
+
// given / when / then
|
|
62
|
+
expect(EVIDENCE_RELATIVE_ROOT).toBe(join(".omo", "evidence", "omo-senpi-adapter"))
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
test("#given an unsafe slug #when resolved #then it is rejected before any path is produced", () => {
|
|
66
|
+
// given
|
|
67
|
+
const repoRoot = makeGitRoot()
|
|
68
|
+
const unsafeSlugs = [
|
|
69
|
+
"",
|
|
70
|
+
" ",
|
|
71
|
+
".",
|
|
72
|
+
"..",
|
|
73
|
+
"../escape",
|
|
74
|
+
"nested/slug",
|
|
75
|
+
"nested\\slug",
|
|
76
|
+
"/absolute",
|
|
77
|
+
join(repoRoot, "absolute"),
|
|
78
|
+
"local-ignore/qa-evidence/20260819-senpi",
|
|
79
|
+
"..%2Fescape",
|
|
80
|
+
"-leading-hyphen",
|
|
81
|
+
"trailing-hyphen-",
|
|
82
|
+
"Upper-Case",
|
|
83
|
+
"under_score",
|
|
84
|
+
]
|
|
85
|
+
|
|
86
|
+
// when / then
|
|
87
|
+
for (const slug of unsafeSlugs) {
|
|
88
|
+
expect(() => resolveEvidenceDir({ repoRoot, slug }), `must reject ${JSON.stringify(slug)}`).toThrow()
|
|
89
|
+
}
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
test("#given a directory that is not a git worktree #when resolved #then it is rejected", () => {
|
|
93
|
+
// given
|
|
94
|
+
const notARepo = mkdtempSync(join(tmpdir(), "senpi-qa-not-git-"))
|
|
95
|
+
cleanupRoots.push(notARepo)
|
|
96
|
+
|
|
97
|
+
// when / then
|
|
98
|
+
expect(() => resolveEvidenceDir({ repoRoot: notARepo, slug: "20260820-senpi-qa-contract" })).toThrow(/git/i)
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
test("#given a rejected slug #when the error surfaces #then it names the offending slug for the operator", () => {
|
|
102
|
+
// given
|
|
103
|
+
const repoRoot = makeGitRoot()
|
|
104
|
+
|
|
105
|
+
// when / then
|
|
106
|
+
expect(() => resolveEvidenceDir({ repoRoot, slug: "local-ignore/qa-evidence/x" })).toThrow(
|
|
107
|
+
/local-ignore\/qa-evidence\/x/,
|
|
108
|
+
)
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
test("#given the documented Node CLI #when valid and invalid slugs run #then stdout and exit codes enforce the contract", () => {
|
|
112
|
+
// given
|
|
113
|
+
const repoRoot = makeGitRoot()
|
|
114
|
+
|
|
115
|
+
// when
|
|
116
|
+
const valid = spawnSync("node", [cliPath, "--repo-root", repoRoot, "--slug", "20260820-node-cli"], {
|
|
117
|
+
encoding: "utf8",
|
|
118
|
+
})
|
|
119
|
+
const invalid = spawnSync(
|
|
120
|
+
"node",
|
|
121
|
+
[cliPath, "--repo-root", repoRoot, "--slug", "local-ignore/qa-evidence/x"],
|
|
122
|
+
{ encoding: "utf8" },
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
// then
|
|
126
|
+
expect(valid.status).toBe(0)
|
|
127
|
+
expect(valid.stdout.trim()).toBe(join(repoRoot, EVIDENCE_RELATIVE_ROOT, "20260820-node-cli"))
|
|
128
|
+
expect(invalid.status).toBe(1)
|
|
129
|
+
expect(invalid.stderr).toContain("local-ignore/qa-evidence/x")
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
test("#given a symlink to the documented Node CLI #when invoked #then the entrypoint still enforces the contract", () => {
|
|
133
|
+
// given
|
|
134
|
+
const repoRoot = makeGitRoot()
|
|
135
|
+
const symlinkPath = join(repoRoot, "resolve-evidence-dir.mjs")
|
|
136
|
+
symlinkSync(cliPath, symlinkPath)
|
|
137
|
+
|
|
138
|
+
// when
|
|
139
|
+
const result = spawnSync(
|
|
140
|
+
"node",
|
|
141
|
+
[symlinkPath, "--repo-root", repoRoot, "--slug", "20260820-symlink-cli"],
|
|
142
|
+
{ encoding: "utf8" },
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
// then
|
|
146
|
+
expect(result.status).toBe(0)
|
|
147
|
+
expect(result.stdout.trim()).toBe(join(repoRoot, EVIDENCE_RELATIVE_ROOT, "20260820-symlink-cli"))
|
|
148
|
+
})
|
|
149
|
+
|
|
150
|
+
test("#given an embedding runner with a missing argv path #when the module is imported #then exports still load", () => {
|
|
151
|
+
// given
|
|
152
|
+
const missingEntrypoint = join(tmpdir(), `missing-senpi-qa-entrypoint-${process.pid}.mjs`)
|
|
153
|
+
const moduleUrl = pathToFileURL(cliPath).href
|
|
154
|
+
|
|
155
|
+
// when
|
|
156
|
+
const result = spawnSync(
|
|
157
|
+
"node",
|
|
158
|
+
[
|
|
159
|
+
"--input-type=module",
|
|
160
|
+
"--eval",
|
|
161
|
+
`process.argv[1] = ${JSON.stringify(missingEntrypoint)}; await import(${JSON.stringify(moduleUrl)})`,
|
|
162
|
+
],
|
|
163
|
+
{ encoding: "utf8" },
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
// then
|
|
167
|
+
expect(result.status).toBe(0)
|
|
168
|
+
expect(result.stdout).toBe("")
|
|
169
|
+
expect(result.stderr).toBe("")
|
|
170
|
+
})
|
|
171
|
+
})
|