Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,12 @@ jobs:
- run: pnpm install --frozen-lockfile
- run: pnpm -r run build
- run: pnpm -r run typecheck
- run: pnpm -r run test # amico-run suite (incl. S31 grep rule) + extension unit suite + @amicode/schema conformance
# Fetch the vendored opencode BEFORE vitest so the OC_BIN-gated integration
# test (opencode config injection + merge) actually RUNS in CI instead of
# self-skipping — the skip was the #25 CI-level false-green (an injection or
# config-merge regression would pass CI because the only test for it skipped).
- run: pnpm --filter amicode-v2 fetch:opencode
- run: pnpm -r run test # amico-run suite (incl. S31 grep rule) + extension unit suite (incl. the opencode inject/merge integration) + @amicode/schema conformance
- name: amico-validate — shipped configs conform + linked bin works (0.1c gate)
run: |
# Exercise the LINKED bin via a dependent (the bin links into amico-run /
Expand Down
8 changes: 8 additions & 0 deletions packages/extension/test/boot_smoke.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,14 @@
// GET /event as an SSE stream (HTTP 200, text/event-stream) against a
// synthesized project, with no LLM creds. Exit 0 = pass.
//
// SCOPE (see #25): this is the binary-liveness gate only — it deliberately does
// NOT set OPENCODE_CONFIG_CONTENT, so it can't (and doesn't claim to) catch a
// regression in the instructions/permission injection or the config merge. That
// injection + merge is asserted against the REAL binary + the REAL
// buildOpencodeConfigContent in test/opencode_config.test.ts ("opencode config
// injection + merge"), which can import the TS builder (this .mjs can't, so
// re-deriving the config here would just risk drift).
//
// Boot + probe logic lives in scripts/opencode_probe.mjs (shared with the
// healthcheck, which derives BOTH the /event gate and the provider signal from a
// single boot); this script asserts the /event gate and exits.
Expand Down
39 changes: 29 additions & 10 deletions packages/extension/test/opencode_config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,25 +66,44 @@ describe('buildOpencodeConfigContent', () => {
})
})

// Integration: confirms opencode 1.17.3 DEEP-merges the injected `permission`
// object over the user's global config rather than shallow-replacing it (which
// would wipe the user's other permission keys). Skipped when the vendored binary
// isn't present (e.g. minimal CI before `fetch:opencode`).
// Integration (#25): boots the REAL opencode binary (`opencode debug config`
// resolves + dumps the merged config, equivalent to GET /config) with the REAL
// buildOpencodeConfigContent as OPENCODE_CONFIG_CONTENT, and asserts the whole
// injection + merge the extension relies on at spawn:
// - the injected `instructions` (the AGENTS.md path) is present — this is the
// regression the old boot_smoke false-green missed: boot_smoke.mjs boots
// WITHOUT OPENCODE_CONFIG_CONTENT, so it stayed green even if the instruction
// injection were removed. This test reds instead.
// - the user's global `model` survives the deep-merge (opencode picks the
// provider from it — the merge must not clobber it);
// - the user's global `permission` keys survive AND our injected permission key
// is added (deep-merge, not shallow-replace — folds in the #22 check).
// Uses the real builder (no transcribed copy → no drift; boot_smoke.mjs can't
// import the TS builder, which is why this lives here). Skipped when the vendored
// binary isn't present (e.g. minimal CI before `fetch:opencode`).
const OC_BIN = join(__dirname, '..', 'vendor', 'opencode', `${process.platform}-${process.arch}`, 'opencode')
describe.skipIf(!existsSync(OC_BIN))('opencode permission merge (1.17.3)', () => {
it('injected permission ADDS keys — the user\'s global permission keys survive', () => {
describe.skipIf(!existsSync(OC_BIN))('opencode config injection + merge (1.17.3)', () => {
it('injects instructions/permission AND preserves the user global model + permission', () => {
const home = mkdtempSync(join(tmpdir(), 'ochome-'))
mkdirSync(join(home, '.config', 'opencode'), { recursive: true })
// A user global config with a distinctive model + permission key — both must
// survive the deep-merge under OPENCODE_CONFIG_CONTENT.
writeFileSync(join(home, '.config', 'opencode', 'opencode.json'),
JSON.stringify({ permission: { doom_loop: 'deny' } })) // a distinctive user-set key
JSON.stringify({ model: 'anthropic/claude-sonnet-4-6', permission: { doom_loop: 'deny' } }))
const agentsPath = join(home, 'AGENTS.md') // the exact file our `instructions` must point at
writeFileSync(agentsPath, '# amico\n')
const out = execFileSync(OC_BIN, ['debug', 'config'], {
encoding: 'utf8',
env: { ...process.env, HOME: home, XDG_CONFIG_HOME: join(home, '.config'),
OPENCODE_CONFIG_CONTENT: buildOpencodeConfigContent('/abs/AGENTS.md', '/ext/templates/solve_template.jl') },
OPENCODE_CONFIG_CONTENT: buildOpencodeConfigContent(agentsPath, '/ext/templates/solve_template.jl') },
})
const cfg = JSON.parse(out)
expect(cfg.permission.doom_loop).toBe('deny') // user's key SURVIVED the deep-merge
expect(typeof cfg.permission.external_directory).toBe('object') // our injected key is present too
// our injection landed (the false-green boot_smoke couldn't catch):
expect(cfg.instructions).toContain(agentsPath) // the AGENTS.md instruction injection
expect(typeof cfg.permission.external_directory).toBe('object') // our injected permission key
// the user's global config SURVIVED the deep-merge:
expect(cfg.model).toBe('anthropic/claude-sonnet-4-6') // provider/model preserved (Q129 needs this)
expect(cfg.permission.doom_loop).toBe('deny') // user permission key preserved (#22)
})
})

Expand Down
Loading